From b544a2889b7fe3b6381c13ddfd08f314a613d1d2 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 13 Dec 2024 13:28:48 +0800 Subject: [PATCH 001/506] 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 90f312968e0..0f9ae95d57b 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 9ec7d9e012d..1b6dbf1cdc9 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 d60d6f7b137..bf7dcd19ecf 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 4a12e18b1dd..61e6767a34f 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 2df452a640e..24b02517348 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 78ccc898909..9ff755f6be6 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 00000000000..ced7ba86be2 --- /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 00000000000..f1f6e72bee3 --- /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 9308e714a6b..68b7503b244 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 00000000000..c22f9c1999d --- /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 b2e5501a13a..00000000000 --- 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 00000000000..f47e82ed8a2 --- /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 00000000000..dbce2f53b8e --- /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 00000000000..c2e1d8ff0d7 --- /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 00000000000..b22b40d1f37 --- /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 00000000000..6ff63c3718d --- /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 00000000000..bad42798c83 --- /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 5431b3edd05..4fbce0d4849 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 62b86d08bd6..f2ec5df321e 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 9bcb6e6926c..00000000000 --- 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 a0b337c5bae..58cf2993fdf 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 1fb369b5cea..4a552dda0fc 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 e13a5de11e8..a62be5a8195 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 00000000000..21a1f4d4fe7 --- /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 0e35a9550c0..e7a530ee646 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 f7612d9281c..0346bb58576 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 96eab871739..ac54eafd4c9 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 ca4a47d309b..d19c5daca06 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 75cee85b150..0b2b9ab4f9e 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 002/506] 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 1b6dbf1cdc9..f9e633348c8 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 00000000000..0d3b33bb8c3 --- /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 6d7acb5b89c..57a2f158ad4 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 d7f41bf0ec0..00000000000 --- 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 003/506] 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 8180b9f5f30..7b149938e1a 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 004/506] 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 c193967d0b5..b5980bd7a20 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 005/506] 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 61e6767a34f..8270aa2caa3 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 a201336ac0f..83b6de43a37 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 df07331cfea..cbef0d9140f 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 0bb17c9036d..9e23b7b0267 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 17642646414..14baa69468b 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 006/506] 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 8862614ea32..da37374f23c 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 8d204eef279..23bb71c5457 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 007/506] 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 00000000000..fe08aebdadf --- /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 e7a530ee646..b8e05e211cb 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 008/506] 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 8544219c2b1..f0e4e4a4a62 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 009/506] 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 8270aa2caa3..6bce25667b8 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 3d56472eedc..add4b73d495 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 010/506] 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 6bce25667b8..fb876060d06 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 011/506] 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 fb876060d06..dc5de1d4057 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 012/506] 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 46d4c937dc5..0f6657bbc34 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 013/506] 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 dc5de1d4057..3fc53573b4e 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 014/506] 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 98ceca3bb5e..9f8490ac99e 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 9ce7cc717e3..8a5f942341c 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 5d974092037..802d6a7a69f 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 57a2f158ad4..2aab79aea62 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 eeeccaaec8b..07956f11fa3 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 79061a41b05..49a5a4cadf9 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 5686eea7abf..7d0178be2fd 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 da37374f23c..24fd2ab841e 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 46710e3c9d1..5686b74ef47 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 23bb71c5457..874f83aa87a 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 d3d291433ba..79a2207e63b 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 015/506] 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 208897ed039..7f978201ae2 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 ae07c1498cf..deed97bb30b 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 b6611d3ede3..4b0954d40b1 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 cc3e0613105..0fb8d552d7a 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 71f3dacc3b2..50e05cde588 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 867d71dff2d..359fbc7f721 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 0a15eb45b88..b4bca12a9e6 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 35975078805..9d21ec6cd71 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 f2c7121016e..0ae7affbcf7 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 10f5cc4fdba..691bed75c9a 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 56a99446c38..d978577ab85 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 90b8d7dcbf3..3f555dcaa66 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 0ff04240b55..756367c21f1 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 d6d243c5026..6671d041bbf 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 839ea1a81db..d826ea8c6b6 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 e017b5b6fae..b837dc276e3 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 003640e05e1..27ce2271380 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 1c8b4dd3d7b..ebedd79d44f 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 016/506] 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 fa35507f849..6a2815aceac 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 017/506] crate --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5949b592585..dae69ead3a1 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 018/506] 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 3fc53573b4e..5dfddf12785 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 dae69ead3a1..f2f3076bef7 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 83b6de43a37..05161438325 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 019/506] 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 f9e633348c8..73e1efe3878 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 9f8490ac99e..64792efe9a5 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 8a5f942341c..ea0db9d52cf 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 802d6a7a69f..f3754848197 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 2aab79aea62..af1bc5fe74a 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 07956f11fa3..5f642daac45 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 49a5a4cadf9..7154ad5abe1 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 7d0178be2fd..63e38acc860 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 24fd2ab841e..1359cb4100c 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 5686b74ef47..79b26d6f07c 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 874f83aa87a..860d05df2a1 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 79a2207e63b..27586a54fce 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 020/506] 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 73e1efe3878..ea525f52532 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 bf7dcd19ecf..46a6dd0c30f 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 5dfddf12785..21f9503858e 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 f2f3076bef7..f5474ae4e37 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 64792efe9a5..1e8e3959606 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 ea0db9d52cf..646113d4a49 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 1776db7a5e7..d8c20985f81 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 b9c4447a213..b9982bce79e 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 7154ad5abe1..ab97225a3c4 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 63e38acc860..347ecd989e4 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 1359cb4100c..b6a25ac55fa 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 860d05df2a1..90c57e67367 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 021/506] 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 c31350b047c..6eb9b059470 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 022/506] 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 f6826fd8720..00000000000 --- 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 023/506] 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 ea525f52532..fe23f942378 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 024/506] 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 e24712efa4d..b609b8cc79e 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 9a086109473..34021d6bf20 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 32c60a4643f..a9112fe6259 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 2ae520f4dfc..126a341cd72 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 41b64264044..606ff671996 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 8031516972e..2605f304e73 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 ced5baf327f..9effbc5893b 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 7b68d821c81..87d4897c2d5 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 b3c5546a6fc..d030bc6f136 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 c0839dc55ac..92d02220ea4 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 74a7f736f24..f59e4c9ff78 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 b40ee411676..8164e19bd81 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 ca86e48e792..47d2f578963 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 025/506] 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 fad447b692f..6cfd29d6c1d 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 026/506] 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 f3754848197..dc85a60059d 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 027/506] 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 cbef0d9140f..24cc60eb481 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 028/506] 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 24cc60eb481..bdfa8f1e2df 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 14baa69468b..f76bd4e944a 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 029/506] 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 1821c529afb..40fe3c3c351 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 030/506] 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 81c3bedae5f..ae9626ff8e8 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 031/506] 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 8888f9e5734..7a2b861c857 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 d8c20985f81..a64c61415ed 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 032/506] 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 691bed75c9a..ba724eed5ee 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 49e3b2c9107..2d8fe9765b2 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 033/506] 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 d978577ab85..f89381a3ff5 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 ede66d78ae7..83cfa2fb23a 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 034/506] 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 21f9503858e..a8cca450fab 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 00000000000..2e8aff64aa5 --- /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 b8e05e211cb..1d46535d5dd 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 035/506] fix: fix crate vulnerabilities (#10407) --- Cargo.lock | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8cca450fab..3247683e73d 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 036/506] 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 f5474ae4e37..28cc25cc159 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 dc85a60059d..87c0dbd3432 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 a29674fece3..a9f3c7388cf 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 3498decd37a..46372a5822f 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 d030bc6f136..2fb2b46db90 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 61efc25eca4..59f103a3138 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 037/506] 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 00000000000..ce1855bdb8b --- /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 038/506] 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 add4b73d495..e4e30106631 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 58cf2993fdf..77b41a7ada8 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 039/506] 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 2605f304e73..1488ffd93cf 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 040/506] 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 ee96f57c851..6b163d6e460 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 041/506] 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 6b163d6e460..cef718cc109 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 042/506] 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 0ae7affbcf7..235e2185a06 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 043/506] 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 b5980bd7a20..cc555270ac8 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 295564457b8..ef8f42648c0 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 8f226914d70..98f19d4e69d 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 044/506] 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 89854769ea4..d7eb8dc69fd 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 045/506] 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 a3437e01f0b..3ad77afe590 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 046/506] 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 a3e3666c680..18bc5e6381e 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 047/506] 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 5a5dcf623ee..748fa1c4043 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 048/506] 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 bdfa8f1e2df..fb7cba3c51c 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 049/506] 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 5f642daac45..dad333ee5b9 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 baef2e2e202..133ff11debd 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 050/506] 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 9d56fbe6f2b..5e38de4a937 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 0f9ae95d57b..690e3cf44fb 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 fe23f942378..30b75af6478 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 46a6dd0c30f..b0b7a57258f 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 00000000000..d80e69aa84a --- /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 3247683e73d..6fab0e8fa2e 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 7f978201ae2..2fde813bcdb 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 2d8fe9765b2..1d83b5744c3 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 00000000000..49c6b24a7a8 --- /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 693699042b1..00000000000 --- 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 259d01e9dd4..00000000000 --- 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 5ebc3a28706..00000000000 --- 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 95169df8e2c..00000000000 --- 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 0be78842868..00000000000 --- 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 d4601c0f98e..00000000000 --- 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 2fc0d9040e6..00000000000 --- 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 bfc79871554..00000000000 --- 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 761d916e4f8..00000000000 --- 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 dd4abaf9d9b..00000000000 --- 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 1488ffd93cf..00000000000 --- 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 10979f520e9..00000000000 --- 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 36a68550fa5..00000000000 --- 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 90a5d6d402e..00000000000 --- 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 5c04cc97b92..00000000000 --- 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 60c8714d821..00000000000 --- 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 dd83a87385b..00000000000 --- 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 5dc004a81b7..00000000000 --- 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 7481631ace1..00000000000 --- 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 57d9b68fe34..00000000000 --- 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 34d2c5109f5..00000000000 --- 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 4cb0bf204b5..00000000000 --- 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 17f360ff90c..00000000000 --- 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 68abd42df9a..00000000000 --- 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 05161438325..6833ad34f70 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 294ab97cc4f..14db9b5d895 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 153740c28a3..93e45336342 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 051/506] 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 803a4343ce2..a867a2d3fcc 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 6d68a7352f9..ca620608adb 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 d2bb2feb77f..4bf17a2fee3 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 648de19b069..662ac02a542 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 e4e30106631..7ee9b3d61d5 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 11b497fb3bc..244f38ed57b 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 eb3b8e1ce39..747dbbe76db 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 93e45336342..326d128777f 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 5d3aeca85ea..51939515c24 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 733405a37f7..5bc58da45d3 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 052/506] 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 30b75af6478..da96b9af077 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 b0b7a57258f..7cb1f801c52 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 6fab0e8fa2e..e8ed6c42e12 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 28cc25cc159..917b49a879c 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 1e8e3959606..a5ae4ef78c9 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 646113d4a49..25889d5b7d0 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 a64c61415ed..3871449996e 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 b9982bce79e..5553c718811 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 ab97225a3c4..f27fd6d638d 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 347ecd989e4..d56838d3c3f 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 b6a25ac55fa..771c8a12e7f 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 90c57e67367..eb4a9a7ad38 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 053/506] 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 e8ed6c42e12..23c75024446 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 054/506] 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 da96b9af077..bf9469c0e57 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 055/506] 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 5e38de4a937..2d5affeef52 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 bf9469c0e57..bb43c39d3ec 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 40fe3c3c351..ecfb444efea 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 056/506] 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 23c75024446..de75588adc4 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 51939515c24..108af9f4498 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 057/506] 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 cc555270ac8..22cd390625e 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 00000000000..baefda0519a --- /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 00000000000..89a57456327 --- /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 00000000000..1d944ed5d6c --- /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 00000000000..dbe4ddd2deb --- /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 00000000000..1f8dcb411bd --- /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 058/506] Update README.md (#10586) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22cd390625e..1fcb65ff3af 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 059/506] Update README.md (#10587) --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 1fcb65ff3af..1c4d6be4afe 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 060/506] 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 d877f022682..a5099f48761 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 061/506] 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 0b78aa68560..2979c1aaad5 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 062/506] 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 00000000000..56258e4e0a9 --- /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 063/506] 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 de75588adc4..4b762259505 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 923606a8277..cc36b3c2308 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 00000000000..18da50b446c --- /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 1d46535d5dd..9d09c526423 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 23d7706d473..5264d5bfd1e 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 fe0a77e39d3..255a00e0fd5 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 92d02220ea4..6c0136128a1 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 064/506] 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 30055740ed8..6bdd2293aa6 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 5d1aa086ddb..3b931a4a651 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 6f8381a6ad4..e065be215c3 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 49c6b24a7a8..79f8ac2d68e 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 a4bfc1aef69..4548cdbea1a 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 065/506] 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 bb43c39d3ec..a795bfaeda0 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 066/506] 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 79f8ac2d68e..97266d7c180 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 6833ad34f70..bdef9c7ed0f 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 067/506] 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 8c7be0786ed..00000000000 --- 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 85e6ff194b9..00000000000 --- 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 70f9eaeb270..00000000000 --- 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 ea8c4bf7f35..00000000000 --- 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 0b2e8ca2b8d..00000000000 --- 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 0584b469018..00000000000 --- 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 0283266bf7d..00000000000 --- 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 9f856fa2e5e..00000000000 --- 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 a342f1a9afc..00000000000 --- 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 e9f8d7ed3cf..00000000000 --- 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 ea32a028a3c..00000000000 --- 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 b43bd364f46..00000000000 --- 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 068/506] 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 00000000000..8c7be0786ed --- /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 From ce5f0d513f5e613e6c8fd055fbb71475707d7d46 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 31 Jan 2025 16:54:57 +0800 Subject: [PATCH 069/506] 2024 -> 2025 --- Cargo.toml | 2 +- flutter/macos/Runner/Configs/AppInfo.xcconfig | 2 +- flutter/windows/runner/Runner.rc | 2 +- libs/portable/Cargo.toml | 2 +- src/ui/index.tis | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 917b49a879c..637f515d299 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -181,7 +181,7 @@ members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "lib exclude = ["vdi/host", "examples/custom_plugin"] [package.metadata.winres] -LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved." +LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved." ProductName = "RustDesk" FileDescription = "RustDesk Remote Desktop" OriginalFilename = "rustdesk.exe" diff --git a/flutter/macos/Runner/Configs/AppInfo.xcconfig b/flutter/macos/Runner/Configs/AppInfo.xcconfig index f095b674e4a..eabc428e5ec 100644 --- a/flutter/macos/Runner/Configs/AppInfo.xcconfig +++ b/flutter/macos/Runner/Configs/AppInfo.xcconfig @@ -11,4 +11,4 @@ PRODUCT_NAME = RustDesk PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2024 Purslane Ltd. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2025 Purslane Ltd. All rights reserved. diff --git a/flutter/windows/runner/Runner.rc b/flutter/windows/runner/Runner.rc index 7e0fe9b31f6..ab1b7e06fed 100644 --- a/flutter/windows/runner/Runner.rc +++ b/flutter/windows/runner/Runner.rc @@ -93,7 +93,7 @@ BEGIN VALUE "FileDescription", "RustDesk Remote Desktop" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "rustdesk" "\0" - VALUE "LegalCopyright", "Copyright © 2024 Purslane Ltd. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright © 2025 Purslane Ltd. All rights reserved." "\0" VALUE "OriginalFilename", "rustdesk.exe" "\0" VALUE "ProductName", "RustDesk" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index 5553c718811..dc82f1ae2dd 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -18,7 +18,7 @@ winapi = { version = "0.3", features = ["winbase"] } native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]} [package.metadata.winres] -LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved." +LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved." ProductName = "RustDesk" OriginalFilename = "rustdesk.exe" FileDescription = "RustDesk Remote Desktop" diff --git a/src/ui/index.tis b/src/ui/index.tis index 2c9b0f9835b..3e5aa8ad065 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -385,7 +385,7 @@ class MyIdMenu: Reactor.Component {

Fingerprint: " + handler.get_fingerprint() + " \
" + translate("Privacy Statement") + "
\
" + translate("Website") + "
\ -
Copyright © 2024 Purslane Ltd.\ +
Copyright © 2025 Purslane Ltd.\
" + handler.get_license() + " \

" + translate("Slogan_tip") + "

\
\ From db3ca6a373f90f550a19c05197c154915a7176ee Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 1 Feb 2025 13:07:27 +0800 Subject: [PATCH 070/506] remove useless code --- examples/custom_plugin/Cargo.toml | 28 ---------------------------- examples/custom_plugin/src/lib.rs | 30 ------------------------------ 2 files changed, 58 deletions(-) delete mode 100644 examples/custom_plugin/Cargo.toml delete mode 100644 examples/custom_plugin/src/lib.rs diff --git a/examples/custom_plugin/Cargo.toml b/examples/custom_plugin/Cargo.toml deleted file mode 100644 index b9ee06ae734..00000000000 --- a/examples/custom_plugin/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "custom_plugin" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -name = "custom_plugin" -path = "src/lib.rs" -crate-type = ["cdylib"] - - -[features] -default = ["flutter"] -flutter = [] - -[dependencies] -lazy_static = "1.4.0" -rustdesk = { path = "../../", version = "1.2.0", features = ["flutter"]} - -[profile.release] -lto = true -codegen-units = 1 -panic = 'abort' -strip = true -#opt-level = 'z' # only have smaller size after strip -rpath = true \ No newline at end of file diff --git a/examples/custom_plugin/src/lib.rs b/examples/custom_plugin/src/lib.rs deleted file mode 100644 index 0b21f3fc854..00000000000 --- a/examples/custom_plugin/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -use librustdesk::api::RustDeskApiTable; -/// This file demonstrates how to write a custom plugin for RustDesk. -use std::ffi::{c_char, c_int, CString}; - -lazy_static::lazy_static! { - pub static ref PLUGIN_NAME: CString = CString::new("A Template Rust Plugin").unwrap(); - pub static ref PLUGIN_ID: CString = CString::new("TemplatePlugin").unwrap(); - // Do your own logic based on the API provided by RustDesk. - pub static ref API: RustDeskApiTable = RustDeskApiTable::default(); -} - -#[no_mangle] -fn plugin_name() -> *const c_char { - return PLUGIN_NAME.as_ptr(); -} - -#[no_mangle] -fn plugin_id() -> *const c_char { - return PLUGIN_ID.as_ptr(); -} - -#[no_mangle] -fn plugin_init() -> c_int { - return 0 as _; -} - -#[no_mangle] -fn plugin_dispose() -> c_int { - return 0 as _; -} From 7a5941de98d276980456282d00ad339921bfa1bb Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 2 Feb 2025 20:31:44 +0800 Subject: [PATCH 071/506] remove devcontainer since not maintained yet --- res/.devcontainer/Dockerfile | 50 ------------------- res/.devcontainer/build.sh | 75 ----------------------------- res/.devcontainer/devcontainer.json | 34 ------------- res/.devcontainer/setup.sh | 23 --------- 4 files changed, 182 deletions(-) delete mode 100644 res/.devcontainer/Dockerfile delete mode 100755 res/.devcontainer/build.sh delete mode 100644 res/.devcontainer/devcontainer.json delete mode 100755 res/.devcontainer/setup.sh diff --git a/res/.devcontainer/Dockerfile b/res/.devcontainer/Dockerfile deleted file mode 100644 index 93fd92ecbce..00000000000 --- a/res/.devcontainer/Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -FROM mcr.microsoft.com/devcontainers/base:ubuntu-22.04 -ENV HOME=/home/vscode -ENV WORKDIR=$HOME/rustdesk - -WORKDIR $HOME -RUN sudo apt update -y && sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev -WORKDIR / - -RUN git clone https://github.com/microsoft/vcpkg -WORKDIR vcpkg -RUN git checkout 2023.04.15 -RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics -ENV VCPKG_ROOT=/vcpkg -RUN $VCPKG_ROOT/vcpkg --disable-metrics install libvpx libyuv opus aom - -WORKDIR / -RUN wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz && tar xzf dep.tar.gz - - -USER vscode -WORKDIR $HOME -RUN wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh -RUN chmod +x rustup.sh -RUN $HOME/rustup.sh -y -RUN $HOME/.cargo/bin/rustup target add aarch64-linux-android -RUN $HOME/.cargo/bin/cargo install cargo-ndk - -# Install Flutter -RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.10.1-stable.tar.xz -RUN tar xf flutter_linux_3.10.1-stable.tar.xz && rm flutter_linux_3.10.1-stable.tar.xz -ENV PATH="$PATH:$HOME/flutter/bin" -RUN dart pub global activate ffigen 5.0.1 - - -# Install packages -RUN sudo apt-get install -y libclang-dev -RUN sudo apt install -y gcc-multilib - -WORKDIR $WORKDIR -ENV ANDROID_NDK_HOME=/opt/android/ndk/22.1.7171670 - -# Somehow try to automate flutter pub get -# https://rustdesk.com/docs/en/dev/build/android/ -# Put below steps in entrypoint.sh -# cd flutter -# wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz -# tar xzf so.tar.gz - -# own /opt/android diff --git a/res/.devcontainer/build.sh b/res/.devcontainer/build.sh deleted file mode 100755 index df87aace74e..00000000000 --- a/res/.devcontainer/build.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash - -set -e - -MODE=${1:---debug} -TYPE=${2:-linux} -MODE=${MODE/*-/} - - -build(){ - pwd - $WORKDIR/entrypoint $1 -} - -build_arm64(){ - CWD=$(pwd) - cd $WORKDIR/flutter - flutter pub get - cd $WORKDIR - $WORKDIR/flutter/ndk_arm64.sh - cp $WORKDIR/target/aarch64-linux-android/release/liblibrustdesk.so $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so - cd $CWD -} - -build_apk(){ - cd $WORKDIR/flutter - MODE=$1 $WORKDIR/flutter/build_android.sh - cd $WORKDIR -} - -key_gen(){ - if [ ! -f $WORKDIR/flutter/android/key.properties ] - then - if [ ! -f $HOME/upload-keystore.jks ] - then - $WORKDIR/.devcontainer/setup.sh key - fi - read -r -p "enter the password used to generate $HOME/upload-keystore.jks\n" password - echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties - else - echo "Believing storeFile is created ref: $WORKDIR/flutter/android/key.properties" - fi -} - -android_build(){ - if [ ! -d $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a ] - then - $WORKDIR/.devcontainer/setup.sh android - fi - build_arm64 - case $1 in - debug) - build_apk debug - ;; - release) - key_gen - build_apk release - ;; - esac -} - -case "$MODE:$TYPE" in - "debug:linux") - build - ;; - "release:linux") - build --release - ;; - "debug:android") - android_build debug - ;; - "release:android") - android_build release - ;; -esac diff --git a/res/.devcontainer/devcontainer.json b/res/.devcontainer/devcontainer.json deleted file mode 100644 index 953196eb319..00000000000 --- a/res/.devcontainer/devcontainer.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "rustdesk", - "build": { - "dockerfile": "./Dockerfile", - "context": "." - }, - "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache", - "workspaceFolder": "/home/vscode/rustdesk", - "postStartCommand": ".devcontainer/build.sh", - "features": { - "ghcr.io/devcontainers/features/java:1": {}, - "ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": { - "PACKAGES": "platform-tools,ndk;23.2.8568313" - } - }, - "customizations": { - "vscode": { - "extensions": [ - "vadimcn.vscode-lldb", - "mutantdino.resourcemonitor", - "rust-lang.rust-analyzer", - "tamasfe.even-better-toml", - "serayuzgur.crates", - "mhutchie.git-graph", - "eamodio.gitlens" - ], - "settings": { - "files.watcherExclude": { - "**/target/**": true - } - } - } - } -} diff --git a/res/.devcontainer/setup.sh b/res/.devcontainer/setup.sh deleted file mode 100755 index c972f47b24a..00000000000 --- a/res/.devcontainer/setup.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -e -case $1 in - android) - # install deps - cd $WORKDIR/flutter - flutter pub get - wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz - tar xzf so.tar.gz - rm so.tar.gz - sudo chown -R $(whoami) $ANDROID_HOME - echo "Setup is Done." - ;; - linux) - echo "Linux Setup" - ;; - key) - echo -e "\n$HOME/upload-keystore.jks is not created.\nLet's create it.\nRemember the password you enter in keytool!" - keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload - ;; -esac - - \ No newline at end of file From a27fa4308102fda390108d9592b1515a3adc966f Mon Sep 17 00:00:00 2001 From: ANB5Dev <90337467+ANB5Dev@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:18:20 +0100 Subject: [PATCH 072/506] Update NL translation: spelling, capitalization, missing entries (#10661) --- src/lang/nl.rs | 240 ++++++++++++++++++++++++------------------------- 1 file changed, 120 insertions(+), 120 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 17bce069519..90c87c32447 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -3,60 +3,60 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), ("Your Desktop", "Uw Bureaublad"), - ("desk_tip", "Uw bureaublad is toegankelijk via de ID en het wachtwoord hieronder."), + ("desk_tip", "Uw bureaublad is toegankelijk met dit ID en wachtwoord."), ("Password", "Wachtwoord"), ("Ready", "Klaar"), ("Established", "Opgezet"), ("connecting_status", "Verbinding maken met het RustDesk netwerk..."), - ("Enable service", "Service Inschakelen"), + ("Enable service", "Service inschakelen"), ("Start service", "Start service"), ("Service is running", "De service loopt."), ("Service is not running", "De service loopt niet"), - ("not_ready_status", "Niet klaar, controleer de netwerkverbinding"), + ("not_ready_status", "Niet verbonden met de server, controleer de netwerkverbinding"), ("Control Remote Desktop", "Beheer Extern Bureaublad"), - ("Transfer file", "Bestand Overzetten"), + ("Transfer file", "Bestand overzetten"), ("Connect", "Verbinden"), - ("Recent sessions", "Recente Behandelingen"), + ("Recent sessions", "Recente sessies"), ("Address book", "Adresboek"), ("Confirmation", "Bevestiging"), - ("TCP tunneling", "TCP tunneling"), + ("TCP tunneling", "TCP-tunneling"), ("Remove", "Verwijder"), ("Refresh random password", "Vernieuw willekeurig wachtwoord"), ("Set your own password", "Stel uw eigen wachtwoord in"), - ("Enable keyboard/mouse", "Toetsenbord/Muis Inschakelen"), - ("Enable clipboard", "Klembord Inschakelen"), - ("Enable file transfer", "Bestandsoverdracht Inschakelen"), - ("Enable TCP tunneling", "TCP tunneling Inschakelen"), + ("Enable keyboard/mouse", "Toetsenbord/muis inschakelen"), + ("Enable clipboard", "Klembord inschakelen"), + ("Enable file transfer", "Bestandsoverdracht inschakelen"), + ("Enable TCP tunneling", "TCP-tunneling inschakelen"), ("IP Whitelisting", "IP Witte Lijst"), - ("ID/Relay Server", "ID/Relay Server"), - ("Import server config", "Importeer Serverconfiguratie"), - ("Export Server Config", "Exporteer Serverconfiguratie"), + ("ID/Relay Server", "ID-/Relayserver"), + ("Import server config", "Importeer serverconfiguratie"), + ("Export Server Config", "Exporteer serverconfiguratie"), ("Import server configuration successfully", "Importeren serverconfiguratie is geslaagd"), ("Export server configuration successfully", "Exporteren serverconfiguratie is geslaagd"), - ("Invalid server configuration", "Ongeldige Serverconfiguratie"), + ("Invalid server configuration", "Ongeldige serverconfiguratie"), ("Clipboard is empty", "Klembord is leeg"), ("Stop service", "Stop service"), ("Change ID", "Wijzig ID"), - ("Your new ID", "Uw nieuw ID"), + ("Your new ID", "Uw nieuwe ID"), ("length %min% to %max%", "lengte %min% tot %max%"), ("starts with a letter", "begint met een letter"), ("allowed characters", "toegestane tekens"), ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), ("Website", "Website"), ("About", "Over"), - ("Slogan_tip", "Ontwikkeld met het hart voor deze chaotische wereld!"), + ("Slogan_tip", "Met hart gemaakt in deze chaotische wereld!"), ("Privacy Statement", "Privacyverklaring"), ("Mute", "Geluid uit"), - ("Build Date", "Versie datum"), + ("Build Date", "Datum"), ("Version", "Versie"), ("Home", "Startpagina"), - ("Audio Input", "Audio Ingang"), + ("Audio Input", "Audioingang"), ("Enhancements", "Verbeteringen"), - ("Hardware Codec", "Hardware Codec"), - ("Adaptive bitrate", "Aangepaste Bitsnelheid"), - ("ID Server", "Server ID"), - ("Relay Server", "Relay Server"), - ("API Server", "API Server"), + ("Hardware Codec", "Hardwarecodec"), + ("Adaptive bitrate", "Bitrate automatisch aanpassen"), + ("ID Server", "ID-server"), + ("Relay Server", "Relay-server"), + ("API Server", "API-server"), ("invalid_http", "Moet beginnen met http:// of https://"), ("Invalid IP", "Ongeldig IP"), ("Invalid format", "Ongeldig formaat"), @@ -68,43 +68,43 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Close", "Sluit"), ("Retry", "Probeer opnieuw"), ("OK", "OK"), - ("Password Required", "Wachtwoord vereist"), + ("Password Required", "Wachtwoord Vereist"), ("Please enter your password", "Geef uw wachtwoord in"), ("Remember password", "Wachtwoord onthouden"), - ("Wrong Password", "Verkeerd wachtwoord"), + ("Wrong Password", "Verkeerd Wachtwoord"), ("Do you want to enter again?", "Wilt u het opnieuw invoeren?"), ("Connection Error", "Fout bij verbinding"), ("Error", "Fout"), - ("Reset by the peer", "Reset door de peer"), + ("Reset by the peer", "Door de peer gereset"), ("Connecting...", "Verbinding maken..."), - ("Connection in progress. Please wait.", "Verbinding in uitvoering. Even geduld a.u.b."), + ("Connection in progress. Please wait.", "Verbinding wordt gemaakt. Even geduld a.u.b."), ("Please try 1 minute later", "Probeer 1 minuut later"), - ("Login Error", "Login Fout"), + ("Login Error", "Loginfout"), ("Successful", "Geslaagd"), ("Connected, waiting for image...", "Verbonden, wacht op beeld..."), ("Name", "Naam"), ("Type", "Type"), ("Modified", "Gewijzigd"), ("Size", "Grootte"), - ("Show Hidden Files", "Toon verborgen bestanden"), - ("Receive", "Ontvangen"), - ("Send", "Verzenden"), + ("Show Hidden Files", "Toon Verborgen Bestanden"), + ("Receive", "Ontvang"), + ("Send", "Verzend"), ("Refresh File", "Bestand Verversen"), ("Local", "Lokaal"), - ("Remote", "Op afstand"), + ("Remote", "Op Afstand"), ("Remote Computer", "Externe Computer"), ("Local Computer", "Lokale Computer"), ("Confirm Delete", "Bevestig Verwijderen"), ("Delete", "Verwijder"), ("Properties", "Eigenschappen"), - ("Multi Select", "Meervoudig selecteren"), + ("Multi Select", "Meervoudig Selecteren"), ("Select All", "Selecteer Alle"), - ("Unselect All", "De-selecteer alles"), + ("Unselect All", "De-selecteer Alle"), ("Empty Directory", "Lege Map"), ("Not an empty directory", "Geen lege map"), ("Are you sure you want to delete this file?", "Weet u zeker dat u dit bestand wilt verwijderen?"), ("Are you sure you want to delete this empty directory?", "Weet u zeker dat u deze lege map wilt verwijderen?"), - ("Are you sure you want to delete the file of this directory?", "Weet u zeker dat u het bestand uit deze map wilt verwijderen?"), + ("Are you sure you want to delete the file of this directory?", "Weet u zeker dat u de bestanden uit deze map wilt verwijderen?"), ("Do this for all conflicts", "Doe dit voor alle conflicten"), ("This is irreversible!", "Dit is onomkeerbaar!"), ("Deleting", "Verwijderen"), @@ -112,16 +112,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Waiting", "Wachten"), ("Finished", "Voltooid"), ("Speed", "Snelheid"), - ("Custom Image Quality", "Aangepaste beeldkwaliteit"), + ("Custom Image Quality", "Aangepaste Beeldkwaliteit"), ("Privacy mode", "Privacymodus"), ("Block user input", "Gebruikersinvoer blokkeren"), - ("Unblock user input", "Gebruikersinvoer opheffen"), + ("Unblock user input", "Gebruikersinvoer deblokkeren"), ("Adjust Window", "Venster Aanpassen"), ("Original", "Origineel"), ("Shrink", "Verkleinen"), ("Stretch", "Uitrekken"), ("Scrollbar", "Schuifbalk"), - ("ScrollAuto", "Auto Schuiven"), + ("ScrollAuto", "Automatisch schuiven"), ("Good image quality", "Goede beeldkwaliteit"), ("Balanced", "Gebalanceerd"), ("Optimize reaction time", "Optimaliseer reactietijd"), @@ -130,8 +130,8 @@ 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 Ctrl + Alt + Del", "Ctrl + Alt + Del Invoegen"), - ("Insert Lock", "Vergrendeling Invoegen"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Invoeren"), + ("Insert Lock", "Vergrendelen"), ("Refresh", "Vernieuwen"), ("ID does not exist", "ID bestaat niet"), ("Failed to connect to rendezvous server", "Verbinding met rendez-vous-server mislukt"), @@ -139,32 +139,32 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remote desktop is offline", "Extern bureaublad is offline"), ("Key mismatch", "Code onjuist"), ("Timeout", "Time-out"), - ("Failed to connect to relay server", "Verbinding met relayserver mislukt"), - ("Failed to connect via rendezvous server", "Verbinding via rendez-vous-server mislukt"), - ("Failed to connect via relay server", "Verbinding via relaisserver mislukt"), - ("Failed to make direct connection to remote desktop", "Onmogelijk direct verbinding te maken met extern bureaublad"), + ("Failed to connect to relay server", "Verbinden met relayserver mislukt"), + ("Failed to connect via rendezvous server", "Verbinden via rendez-vous-server mislukt"), + ("Failed to connect via relay server", "Verbinden via relaisserver mislukt"), + ("Failed to make direct connection to remote desktop", "Direct verbinden met extern bureaublad is mislukt"), ("Set Password", "Wachtwoord Instellen"), ("OS Password", "OS Wachtwoord"), - ("install_tip", "U gebruikt een niet geïnstalleerde versie. Als gevolg van UAC-beperkingen is het in sommige gevallen niet mogelijk om als controleterminal de muis en het toetsenbord te bedienen of het scherm over te nemen. Klik op de knop hieronder om RustDesk op het systeem te installeren om het bovenstaande probleem te voorkomen."), + ("install_tip", "Door UAC-beperkingen lukt het niet altijd om uw bureaublad op afstand te bedienen. Installeer RustDesk op het systeem om dit probleem te voorkomen."), ("Click to upgrade", "Klik voor upgrade"), ("Click to download", "Klik om te downloaden"), ("Click to update", "Klik om bij te werken"), ("Configure", "Configureren"), - ("config_acc", "Om uw bureaublad op afstand te kunnen bedienen, moet u RustDesk \"toegankelijkheid\" toestemming geven."), - ("config_screen", "Om toegang te krijgen tot het externe bureaublad, moet u RustDesk de toestemming \"schermregistratie\" geven."), + ("config_acc", "Om uw bureaublad op afstand te kunnen bedienen, moet u RustDesk toestemming voor \"toegankelijkheid\" geven."), + ("config_screen", "Om toegang te krijgen tot het externe bureaublad, moet u RustDesk toestemming voor \"schermregistratie\" geven."), ("Installing ...", "Installeren ..."), ("Install", "Installeer"), ("Installation", "Installatie"), - ("Installation Path", "Installatie Pad"), - ("Create start menu shortcuts", "Startmenu snelkoppelingen maken"), + ("Installation Path", "Locatie"), + ("Create start menu shortcuts", "Startmenu-snelkoppelingen maken"), ("Create desktop icon", "Bureaubladpictogram maken"), ("agreement_tip", "Het starten van de installatie betekent het accepteren van de licentieovereenkomst."), ("Accept and Install", "Accepteren en installeren"), ("End-user license agreement", "Licentieovereenkomst eindgebruiker"), ("Generating ...", "Genereert ..."), ("Your installation is lower version.", "Uw installatie is een lagere versie."), - ("not_close_tcp_tip", "Gelieve dit venster niet te sluiten wanneer u de tunnel gebruikt"), - ("Listening ...", "Luisteren ..."), + ("not_close_tcp_tip", "Sluit dit venster niet zolang u de tunnel gebruikt"), + ("Listening ...", "Luistert ..."), ("Remote Host", "Externe Host"), ("Remote Port", "Externe Poort"), ("Action", "Actie"), @@ -172,7 +172,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Lokale Poort"), ("Local Address", "Lokaal Adres"), ("Change Local Port", "Wijzig Lokale Poort"), - ("setup_server_tip", "Als u een snellere verbindingssnelheid nodig heeft, kunt u ervoor kiezen om uw eigen server aan te maken"), + ("setup_server_tip", "Als u een hogere verbindingssnelheid nodig heeft, kunt u ervoor kiezen om uw eigen server aan te maken"), ("Too short, at least 6 characters.", "Te kort, minstens 6 tekens."), ("The confirmation is not identical.", "De bevestiging is niet identiek."), ("Permissions", "Machtigingen"), @@ -194,10 +194,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Rename", "Naam wijzigen"), ("Space", "Spatie"), ("Create desktop shortcut", "Snelkoppeling op bureaublad maken"), - ("Change Path", "Pad wijzigen"), + ("Change Path", "Pad Wijzigen"), ("Create Folder", "Map Maken"), ("Please enter the folder name", "Geef de mapnaam op"), - ("Fix it", "Repareer het"), + ("Fix it", "Repareer"), ("Warning", "Waarschuwing"), ("Login screen using Wayland is not supported", "Aanmeldingsscherm via Wayland wordt niet ondersteund"), ("Reboot required", "Opnieuw opstarten vereist"), @@ -205,20 +205,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("x11 expected", "x11 verwacht"), ("Port", "Poort"), ("Settings", "Instellingen"), - ("Username", "Gebruikersnaam"), + ("Username", "Gebruiker"), ("Invalid port", "Ongeldige poort"), ("Closed manually by the peer", "Handmatig gesloten door de peer"), - ("Enable remote configuration modification", "Wijziging configuratie op afstand inschakelen"), + ("Enable remote configuration modification", "Configuratiewijziging op afstand inschakelen"), ("Run without install", "Uitvoeren zonder installatie"), - ("Connect via relay", "Verbinden via relais"), + ("Connect via relay", "Verbinden via relay"), ("Always connect via relay", "Altijd verbinden via relay"), - ("whitelist_tip", "Alleen een IP-adres op de witte lijst krijgt toegang tot mijn toestel"), + ("whitelist_tip", "Alleen IP-adressen op de witte lijst krijgen toegang tot mijn toestel"), ("Login", "Log In"), ("Verify", "Controleer"), ("Remember me", "Herinner mij"), ("Trust this device", "Vertrouw dit apparaat"), - ("Verification code", "Verificatie code"), - ("verification_tip", "Er is een nieuw apparaat gedetecteerd en er is een verificatiecode naar het geregistreerde e-mailadres gestuurd, voer de verificatiecode in om de verbinding voort te zetten."), + ("Verification code", "Verificatiecode"), + ("verification_tip", "Er is een verificatiecode naar het geregistreerde e-mailadres gestuurd, voer de verificatiecode in om de verbinding voort te zetten."), ("Logout", "Log Uit"), ("Tags", "Labels"), ("Search ID", "Zoek ID"), @@ -238,24 +238,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remove from Favorites", "Verwijderen uit Favorieten"), ("Empty", "Leeg"), ("Invalid folder name", "Ongeldige mapnaam"), - ("Socks5 Proxy", "Socks5 Proxy"), - ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Socks5 Proxy", "SOCKS5 Proxy"), + ("Socks5/Http(s) Proxy", "SOCKS5/HTTP(S) Proxy"), ("Discovered", "Ontdekt"), - ("install_daemon_tip", "Om bij het opstarten van de computer te kunnen beginnen, moet u de systeemservice installeren."), - ("Remote ID", "Externe ID"), + ("install_daemon_tip", "Om te starten bij het opstarten van de computer, moet u de systeemservice installeren."), + ("Remote ID", "Extern ID"), ("Paste", "Plakken"), - ("Paste here?", "Hier plakken"), + ("Paste here?", "Hier plakken?"), ("Are you sure to close the connection?", "Weet u zeker dat u de verbinding wilt sluiten?"), ("Download new version", "Download nieuwe versie"), - ("Touch mode", "Aanraak modus"), + ("Touch mode", "Aanraakmodus"), ("Mouse mode", "Muismodus"), ("One-Finger Tap", "Een-Vinger Tik"), ("Left Mouse", "Linkermuis"), ("One-Long Tap", "Een-Vinger-Lange-Tik"), ("Two-Finger Tap", "Twee-Vingers-Tik"), - ("Right Mouse", "Rechter muis"), + ("Right Mouse", "Rechtermuis"), ("One-Finger Move", "Een-Vinger-Verplaatsing"), - ("Double Tap & Move", "Dubbel Tik en Verplaatsen"), + ("Double Tap & Move", "Dubbel-Tik en Verplaatsen"), ("Mouse Drag", "Muis Slepen"), ("Three-Finger vertically", "Drie-Vinger verticaal"), ("Mouse Wheel", "Muiswiel"), @@ -304,18 +304,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Turned off", "Uitgeschakeld"), ("Language", "Taal"), ("Keep RustDesk background service", "RustDesk achtergronddienst behouden"), - ("Ignore Battery Optimizations", "Negeer Batterij Optimalisaties"), + ("Ignore Battery Optimizations", "Negeer Batterij-optimalisaties"), ("android_open_battery_optimizations_tip", "Ga naar de volgende pagina met instellingen"), ("Start on boot", "Starten bij Opstarten"), - ("Start the screen sharing service on boot, requires special permissions", "Start de schermdelings service bij het opstarten, vereist speciale rechten"), + ("Start the screen sharing service on boot, requires special permissions", "Start de schermdelingsservice bij het opstarten, vereist speciale rechten"), ("Connection not allowed", "Verbinding niet toegestaan"), - ("Legacy mode", "Verouderde modus"), - ("Map mode", "Map mode"), + ("Legacy mode", "Legacymodus"), + ("Map mode", "Mapmodus"), ("Translate mode", "Vertaalmodus"), ("Use permanent password", "Gebruik permanent wachtwoord"), ("Use both passwords", "Gebruik beide wachtwoorden"), ("Set permanent password", "Stel permanent wachtwoord in"), - ("Enable remote restart", "Schakel Herstart op afstand in"), + ("Enable remote restart", "Herstart op afstand inschakelen"), ("Restart remote device", "Apparaat op afstand herstarten"), ("Are you sure you want to restart", "Weet u zeker dat u wilt herstarten"), ("Restarting remote device", "Apparaat op afstand herstarten"), @@ -336,19 +336,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Relaisverbinding"), ("Secure Connection", "Beveiligde Verbinding"), ("Insecure Connection", "Onveilige Verbinding"), - ("Scale original", "Oorspronkelijke schaal"), - ("Scale adaptive", "Schaalaanpassing"), + ("Scale original", "Oorspronkelijk formaat"), + ("Scale adaptive", "Automatisch schalen"), ("General", "Algemeen"), ("Security", "Beveiliging"), ("Theme", "Thema"), ("Dark Theme", "Donker Thema"), - ("Light Theme", "Lichte Thema"), + ("Light Theme", "Licht Thema"), ("Dark", "Donker"), ("Light", "Licht"), - ("Follow System", "Volg Systeem"), - ("Enable hardware codec", "Hardware codec inschakelen"), + ("Follow System", "Volg systeem"), + ("Enable hardware codec", "Hardwarecodec inschakelen"), ("Unlock Security Settings", "Beveiligingsinstellingen vrijgeven"), - ("Enable audio", "Audio Inschakelen"), + ("Enable audio", "Audio inschakelen"), ("Unlock Network Settings", "Netwerkinstellingen Vrijgeven"), ("Server", "Server"), ("Direct IP Access", "Directe IP toegang"), @@ -363,25 +363,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin Toolbar", "Werkbalk Losmaken"), ("Recording", "Opnemen"), ("Directory", "Map"), - ("Automatically record incoming sessions", "Automatisch inkomende sessies opnemen"), - ("Automatically record outgoing sessions", ""), - ("Change", "Wissel"), + ("Automatically record incoming sessions", "Inkomende sessies automatisch opnemen"), + ("Automatically record outgoing sessions", "Uitgaande sessies automatisch opnemen"), + ("Change", "Aanpassen"), ("Start session recording", "Start de sessieopname"), ("Stop session recording", "Stop de sessieopname"), - ("Enable recording session", "Opnamesessie Activeren"), + ("Enable recording session", "Sessieopname activeren"), ("Enable LAN discovery", "LAN-detectie inschakelen"), - ("Deny LAN discovery", "LAN-detectie Weigeren"), + ("Deny LAN discovery", "LAN-detectie weigeren"), ("Write a message", "Schrijf een bericht"), - ("Prompt", "Verzoek"), + ("Prompt", "Melding"), ("Please wait for confirmation of UAC...", "Wacht op bevestiging van UAC..."), ("elevated_foreground_window_tip", "Het momenteel geopende venster van de op afstand bediende computer vereist hogere rechten. Daarom is het momenteel niet mogelijk de muis en het toetsenbord te gebruiken. Vraag de gebruiker wiens computer u op afstand bedient om het venster te minimaliseren of de rechten te verhogen. Om dit probleem in de toekomst te voorkomen, wordt aanbevolen de software te installeren op de op afstand bediende computer."), ("Disconnected", "Afgesloten"), ("Other", "Andere"), ("Confirm before closing multiple tabs", "Bevestig voordat u meerdere tabbladen sluit"), - ("Keyboard Settings", "Toetsenbord instellingen"), + ("Keyboard Settings", "Toetsenbordinstellingen"), ("Full Access", "Volledige Toegang"), ("Screen Share", "Scherm Delen"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vereist Ubuntu 21.04 of een hogere versie."), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vereist Ubuntu 21.04 of hoger."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vereist een hogere versie van Linux distro. Probeer X11 desktop of verander van OS."), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Selecteer het scherm dat moet worden gedeeld (Bediening aan de kant van de peer)."), @@ -390,18 +390,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "of"), ("Continue with", "Ga verder met"), ("Elevate", "Verhoog"), - ("Zoom cursor", "Cursor Zoomen"), + ("Zoom cursor", "Zoom cursor"), ("Accept sessions via password", "Sessies accepteren via wachtwoord"), ("Accept sessions via click", "Sessies accepteren via klik"), - ("Accept sessions via both", "Accepteer sessies via beide"), + ("Accept sessions via both", "Accepteer sessies via klik of wachtwoord"), ("Please wait for the remote side to accept your session request...", "Wacht tot de andere kant uw sessieverzoek accepteert..."), ("One-time Password", "Eenmalig Wachtwoord"), - ("Use one-time password", "Gebruik een eenmalig Wachtwoord"), - ("One-time password length", "Eenmalig Wachtwoordlengte"), + ("Use one-time password", "Gebruik een eenmalig wachtwoord"), + ("One-time password length", "Lengte eenmalig wachtwoord"), ("Request access to your device", "Toegang tot uw toestel aanvragen"), ("Hide connection management window", "Verberg het venster voor verbindingsbeheer"), ("hide_cm_tip", "Dit kan alleen als de toegang via een permanent wachtwoord verloopt."), - ("wayland_experiment_tip", "Wayland ondersteuning is slechts experimenteel. Gebruik alsjeblieft X11 als u onbeheerde toegang nodig hebt."), + ("wayland_experiment_tip", "Wayland ondersteuning is slechts experimenteel. Gebruik alstublieft X11 als u onbeheerde toegang nodig heeft."), ("Right click to select tabs", "Rechts klikken om tabbladen te selecteren"), ("Skipped", "Overgeslagen"), ("Add to address book", "Toevoegen aan Adresboek"), @@ -412,7 +412,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Selecteer lokaal toetsenbord"), ("software_render_tip", "Als u een NVIDIA grafische kaart hebt en het externe venster sluit onmiddellijk na verbinding, kan het helpen om het nieuwe stuurprogramma te installeren en te kiezen voor software rendering. Een software herstart is vereist."), ("Always use software rendering", "Gebruik altijd software rendering"), - ("config_input", "Om het externe bureaublad vanaf het toetsenbord te kunnen bedienen, moet u RustDesk de rechten \"Invoerbewaking\" geven."), + ("config_input", "Om het externe bureaublad vanaf het toetsenbord te kunnen bedienen, moet u RustDesk de rechten \"Invoer vastleggen\" geven."), ("config_microphone", "Om op afstand te kunnen chatten, moet u RustDesk 'Audio opnemen' rechten geven."), ("request_elevation_tip", "U kunt ook meer rechten vragen als iemand aan de andere kant aanwezig is."), ("Wait", "Wacht"), @@ -433,20 +433,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Medium", "Middelmatig"), ("Strong", "Sterk"), ("Switch Sides", "Wissel van kant"), - ("Please confirm if you want to share your desktop?", "Bevestig als u uw bureaublad wilt delen?"), + ("Please confirm if you want to share your desktop?", "Bevestig dat u uw bureaublad wilt delen?"), ("Display", "Weergave"), - ("Default View Style", "Standaard Weergave Stijl"), - ("Default Scroll Style", "Standaard Scroll Stijl"), + ("Default View Style", "Standaard Weergavestijl"), + ("Default Scroll Style", "Standaard Scrollstijl"), ("Default Image Quality", "Standaard Beeldkwaliteit"), ("Default Codec", "Standaard Codec"), ("Bitrate", "Bitrate"), ("FPS", "FPS"), ("Auto", "Auto"), - ("Other Default Options", "Andere Standaardopties"), + ("Other Default Options", "Overige Standaardinstellingen"), ("Voice call", "Spraakoproep"), - ("Text chat", "Tekst chat"), + ("Text chat", "Tekstchat"), ("Stop voice call", "Stop spraakoproep"), - ("relay_hint_tip", "Indien een directe verbinding niet mogelijk is, kunt u proberen verbinding te maken via een Relay Server. \nAls u bij de eerste poging een relaisverbinding tot stand wilt brengen, kunt u het achtervoegsel \"/r\" toevoegen aan het ID of de optie \"Altijd verbinden via relaisserver\" selecteren op de externe terminal."), + ("relay_hint_tip", "Indien een directe verbinding niet mogelijk is, kunt u proberen verbinding te maken via een Relay Server.\nAls u bij de eerste poging een relaisverbinding tot stand wilt brengen, kunt u het achtervoegsel \"/r\" toevoegen aan het ID of de optie \"Altijd verbinden via relaisserver\" selecteren op de externe terminal."), ("Reconnect", "Opnieuw verbinden"), ("Codec", "Codec"), ("Resolution", "Resolutie"), @@ -459,8 +459,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Minimize", "Minimaliseren"), ("Maximize", "Maximaliseren"), ("Your Device", "Uw Apparaat"), - ("empty_recent_tip", "Oeps, geen actuele situatie!\nTijd om een nieuwe te plannen."), - ("empty_favorite_tip", "Nog geen favoriete Station op afstand? Laat ons iemand vinden om mee te verbinden en voeg hem toe aan uw favorieten!"), + ("empty_recent_tip", "Oeps, geen recente sessies!\nTijd om een nieuwe te plannen."), + ("empty_favorite_tip", "Nog geen favoriete stations op afstand? Laat ons iemand vinden om mee te verbinden en voeg hem toe aan uw favorieten!"), ("empty_lan_tip", "Oh nee, het lijkt erop dat we nog geen extern station hebben ontdekt."), ("empty_address_book_tip", "Oh jee, het lijkt erop dat er momenteel geen externe stations in uw adresboek staan."), ("eg: admin", "bijvoorbeeld: admin"), @@ -469,7 +469,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Me", "Ik"), ("identical_file_tip", "Dit bestand is identiek aan het bestand van het externe station."), ("show_monitors_tip", "Monitoren weergeven in de werkbalk"), - ("View Mode", "Weergave Mode"), + ("View Mode", "Toeschouwermodus"), ("login_linux_tip", "Toegang tot het externe Linux-account"), ("verify_rustdesk_password_tip", "Bevestiging wachtwoord RustDesk"), ("remember_account_tip", "Herinner dit account"), @@ -508,7 +508,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Exit", "Afsluiten"), ("Open", "Open"), ("logout_tip", "Weet u zeker dat u zich wilt afmelden?"), - ("Service", "Service"), + ("Service", "Windows-service"), ("Start", "Start"), ("Stop", "Stop"), ("exceed_max_devices", "Het maximum aantal gecontroleerde apparaten is bereikt."), @@ -544,7 +544,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Timeout in minutes", "Time-out in minuten"), ("auto_disconnect_option_tip", "Inkomende sessies automatisch sluiten bij inactiviteit van de gebruiker"), ("Connection failed due to inactivity", "Automatisch verbinding verbroken wegens inactiviteit"), - ("Check for software update on startup", "Checken voor updates bij opstarten"), + ("Check for software update on startup", "Controleer op updates bij opstarten"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Upgrade RustDesk Server Pro naar versie {} of nieuwer!"), ("pull_group_failed_tip", "Vernieuwen van groep mislukt"), ("Filter by intersection", "Filter op kruising"), @@ -564,7 +564,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Sluit alle"), ("True color (4:4:4)", "Ware kleur (4:4:4)"), ("Enable blocking user input", "Blokkeren van gebruikersinvoer inschakelen"), - ("id_input_tip", "Je kunt een ID, een direct IP of een domein met een poort (:) invoeren. Als je toegang wilt als apparaat op een andere server, voeg dan het serveradres toe (@?key=), bijvoorbeeld \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.Als je toegang wilt als apparaat op een openbare server, voer dan \"@public\" in, voor de openbare server is de sleutel niet nodig."), + ("id_input_tip", "U kunt een ID, een direct IP of een domein met poort (:) invoeren. Als u toegang wilt tot een apparaat op een andere server, voeg dan een serveradres en public key toe (@?key=), bijvoorbeeld \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.Als je toegang wilt als apparaat op een openbare server, voer dan \"@public\" in, voor de openbare server is de sleutel niet nodig."), ("privacy_mode_impl_mag_tip", "Modus 1"), ("privacy_mode_impl_virtual_display_tip", "Modus 2"), ("Enter privacy mode", "Privacymodus openen"), @@ -573,19 +573,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input_source_1_tip", "Invoerbron 1"), ("input_source_2_tip", "Invoerbron 2"), ("Swap control-command key", "Wissel controle-commando toets"), - ("swap-left-right-mouse", "wissel-links-rechts-muis"), + ("swap-left-right-mouse", "Wissel linker- en rechtermuisknop"), ("2FA code", "2FA-code"), ("More", "Meer"), - ("enable-2fa-title", "activeer-2fa-titel"), - ("enable-2fa-desc", "activeer-2fa-desc"), - ("wrong-2fa-code", "foutieve-2fa-code"), - ("enter-2fa-title", "geef-2fa-titel in"), + ("enable-2fa-title", "Tweefactorauthenticatie inschakelen"), + ("enable-2fa-desc", "Stel nu uw authenticator in. U kunt een authenticator-app zoals Authy, Microsoft of Google Authenticator op uw telefoon of desktop gebruiken.\n\nScan de QR-code met uw app en voer de code in die uw app toont om tweefactorauthenticatie in te schakelen."), + ("wrong-2fa-code", "Kan de code niet verifiëren. Controleer of de code en lokale tijdinstellingen correct zijn."), + ("enter-2fa-title", "Tweefactorauthenticatie (2FA)"), ("Email verification code must be 6 characters.", "E-mailverificatiecode moet 6 tekens lang zijn."), ("2FA code must be 6 digits.", "2FA-code moet 6 cijfers lang zijn."), ("Multiple Windows sessions found", "Meerdere Windows-sessies gevonden"), - ("Please select the session you want to connect to", "Selecteer de sessie waarmee je verbinding wilt maken"), + ("Please select the session you want to connect to", "Selecteer de sessie waarmee u verbinding wilt maken"), ("powered_by_me", "Werkt met Rustdesk"), - ("outgoing_only_desk_tip", "Je kan verbinding maken met andere apparaten, maar andere apparaten kunnen geen verbinding maken met dit apparaat."), + ("outgoing_only_desk_tip", "U kan verbinding maken met andere apparaten, maar andere apparaten kunnen geen verbinding maken met u."), ("preset_password_warning", "Dit is een aangepaste editie en wordt geleverd met een vooraf ingesteld wachtwoord. Iedereen die dit wachtwoord kent, kan de volledige controle over het apparaat krijgen."), ("Security Alert", "Beveiligingswaarschuwing"), ("My address book", "Mijn adresboek"), @@ -603,16 +603,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_need_privacy_mode_no_physical_displays_tip", "Geen fysieke schermen, geen privémodus nodig."), ("Follow remote cursor", "Volg de cursor op afstand"), ("Follow remote window focus", "Volg de focus van het venster op afstand"), - ("default_proxy_tip", "Typisch protocol en poort - Socks5 en 1080"), + ("default_proxy_tip", "Standaard protocol en poort: Socks5 en 1080"), ("no_audio_input_device_tip", "Er is geen invoerapparaat gevonden."), ("Incoming", "Inkomend"), ("Outgoing", "Uitgaand"), ("Clear Wayland screen selection", "Wayland-scherm wissen"), - ("clear_Wayland_screen_selection_tip", "Nadat je de schermselectie hebt gewist, kun je het scherm dat je wilt delen opnieuw selecteren."), - ("confirm_clear_Wayland_screen_selection_tip", "Weet je zeker dat je de Wayland-schermselectie wilt wissen?"), + ("clear_Wayland_screen_selection_tip", "Nadat u de schermselectie heeft gewist, kunt u het scherm dat u wilt delen opnieuw selecteren."), + ("confirm_clear_Wayland_screen_selection_tip", "Weet u zeker dat u de Wayland-schermselectie wilt wissen?"), ("android_new_voice_call_tip", "Er is een nieuwe spraakoproep ontvangen. Als u het aanvaardt, schakelt de audio over naar spraakcommunicatie."), ("texture_render_tip", "Pas textuurrendering toe om afbeeldingen vloeiender te maken."), - ("Use texture rendering", "Textuurrendering gebruiken"), + ("Use texture rendering", "Textuurweergave gebruiken"), ("Floating window", "Zwevend venster"), ("floating_window_tip", "Helpt RustDesk op de achtergrond actief te houden"), ("Keep screen on", "Scherm ingeschakeld laten"), @@ -627,9 +627,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Power", "Stroom"), ("Telegram bot", "Telegram bot"), ("enable-bot-tip", "Als u deze functie inschakelt, kunt u een 2FA-code ontvangen van uw bot. Het kan ook fungeren als een verbindingsmelding."), - ("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?"), + ("enable-bot-desc", "1, Open een chat met @BotFather.\n2, Verzend het commando \"/newbot\". Als deze stap voltooid is, ontvangt u 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 u zeker dat u 2FA wilt annuleren?"), + ("cancel-bot-confirm-tip", "Weet u zeker dat u de Telegram-bot wilt annuleren?"), ("About RustDesk", "Over RustDesk"), ("Send clipboard keystrokes", "Klembord toetsaanslagen verzenden"), ("network_error_tip", "Controleer de netwerkverbinding en selecteer 'Opnieuw proberen'."), @@ -648,7 +648,7 @@ 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", "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."), + ("web_id_input_tip", "Je kunt een ID invoeren op dezelfde server, directe IP-toegang wordt niet ondersteund in de webclient.\nAls u toegang wilt tot een apparaat op een andere server, voegt u het serveradres toe (@?key=), bijvoorbeeld,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls u toegang wilt krijgen tot een apparaat op een publieke server, voer dan \"@public\" in, sleutel is niet nodig voor de publieke server."), ("Download", "Downloaden"), ("Upload folder", "Map uploaden"), ("Upload files", "Bestanden uploaden"), From fbba8f0b34ef767dead7199ebd92f4b4a62d390a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 4 Feb 2025 20:33:02 +0800 Subject: [PATCH 073/506] refact: file copy&paste, cross platform (no macOS) (#10671) * feat: unix, file copy&paste Signed-off-by: fufesou * refact: unix file c&p, check peer version Signed-off-by: fufesou * Update pubspec.yaml --------- Signed-off-by: fufesou Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- .github/workflows/flutter-build.yml | 12 +- .github/workflows/playground.yml | 2 +- Cargo.lock | 44 +- Cargo.toml | 2 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- flutter/pubspec.yaml | 2 +- libs/clipboard/Cargo.toml | 2 +- libs/clipboard/src/lib.rs | 63 +- libs/clipboard/src/platform/mod.rs | 75 --- libs/clipboard/src/platform/unix/filetype.rs | 188 ++++++ .../src/platform/{fuse.rs => unix/fuse/cs.rs} | 260 ++------ libs/clipboard/src/platform/unix/fuse/mod.rs | 225 +++++++ .../clipboard/src/platform/unix/local_file.rs | 98 +-- libs/clipboard/src/platform/unix/mod.rs | 602 +----------------- .../src/platform/unix/ns_clipboard.rs | 100 --- .../clipboard/src/platform/unix/serv_files.rs | 231 +++++++ libs/clipboard/src/platform/unix/url.rs | 75 --- libs/clipboard/src/platform/unix/x11.rs | 171 ----- libs/clipboard/src/platform/windows.rs | 1 + 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 | 263 +++++--- src/client/io_loop.rs | 154 +++-- src/clipboard.rs | 461 +++++++++----- src/clipboard_file.rs | 203 ++++++ src/common.rs | 16 +- src/flutter.rs | 19 +- src/flutter_ffi.rs | 14 +- src/ipc.rs | 6 +- src/server.rs | 8 +- src/server/clipboard_service.rs | 106 +-- src/server/connection.rs | 176 ++++- src/server/wayland.rs | 6 - src/ui/header.tis | 9 +- src/ui/remote.rs | 41 ++ src/ui_cm_interface.rs | 110 ++-- src/ui_interface.rs | 19 +- src/ui_session_interface.rs | 16 +- 42 files changed, 2021 insertions(+), 1773 deletions(-) create mode 100644 libs/clipboard/src/platform/unix/filetype.rs rename libs/clipboard/src/platform/{fuse.rs => unix/fuse/cs.rs} (82%) create mode 100644 libs/clipboard/src/platform/unix/fuse/mod.rs delete mode 100644 libs/clipboard/src/platform/unix/ns_clipboard.rs create mode 100644 libs/clipboard/src/platform/unix/serv_files.rs delete mode 100644 libs/clipboard/src/platform/unix/url.rs delete mode 100644 libs/clipboard/src/platform/unix/x11.rs diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index a795bfaeda0..1bdcd1401c3 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: 2025.01.13 VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" - VERSION: "1.3.7" + VERSION: "1.3.8" NDK_VERSION: "r27c" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" @@ -415,7 +415,7 @@ jobs: - name: Build rustdesk run: | - ./build.py --flutter --hwcodec + ./build.py --flutter --hwcodec --unix-file-copy-paste - name: create unsigned dmg if: env.UPLOAD_ARTIFACT == 'true' @@ -796,7 +796,7 @@ jobs: 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 }} + ./build.py --flutter --hwcodec --unix-file-copy-paste ${{ matrix.job.extra-build-args }} - name: create unsigned dmg if: env.UPLOAD_ARTIFACT == 'true' @@ -1554,7 +1554,7 @@ jobs: export JOBS="" fi echo $JOBS - cargo build --lib $JOBS --features hwcodec,flutter --release + cargo build --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release rm -rf target/release/deps target/release/build rm -rf ~/.cargo @@ -1706,7 +1706,7 @@ jobs: deb_arch: amd64, sciter_arch: x64, vcpkg-triplet: x64-linux, - extra_features: ",hwcodec", + extra_features: ",hwcodec,unix-file-copy-paste", } - { arch: armv7, @@ -1716,7 +1716,7 @@ jobs: deb_arch: armhf, sciter_arch: arm32, vcpkg-triplet: arm-linux, - extra_features: "", + extra_features: ",unix-file-copy-paste", } steps: - name: Export GitHub Actions cache environment variables diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 7cb1f801c52..b98cc9618b5 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.7" + VERSION: "1.3.8" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/Cargo.lock b/Cargo.lock index 4b762259505..12057fc8fbc 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#747ab2d9b40a5c9c5102051cf3b0bb38b4845e60" +source = "git+https://github.com/rustdesk-org/arboard#4e16bad260ea05dd7dcdb68cc7549dad3920b940" dependencies = [ "clipboard-win", "core-graphics 0.23.2", @@ -234,6 +234,7 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "parking_lot", + "percent-encoding", "serde 1.0.203", "serde_derive", "windows-sys 0.48.0", @@ -1707,7 +1708,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]] @@ -2231,17 +2232,17 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "fuser" -version = "0.13.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21370f84640642c8ea36dfb2a6bfc4c55941f476fcf431f6fef25a5ddcf0169b" +checksum = "53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369" dependencies = [ "libc", "log", "memchr", + "nix 0.29.0", "page_size", - "pkg-config", "smallvec", - "zerocopy 0.6.6", + "zerocopy 0.8.14", ] [[package]] @@ -4175,7 +4176,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 2.0.2", "proc-macro2 1.0.86", "quote 1.0.36", "syn 2.0.68", @@ -4508,9 +4509,9 @@ dependencies = [ [[package]] name = "page_size" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b7663cbd190cfd818d08efa8497f6cd383076688c49a391ef7c0d03cd12b561" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" dependencies = [ "libc", "winapi 0.3.9", @@ -5506,7 +5507,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.7" +version = "1.3.8" dependencies = [ "android-wakelock", "android_logger", @@ -5606,7 +5607,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.7" +version = "1.3.8" dependencies = [ "brotli", "dirs 5.0.1", @@ -8076,28 +8077,27 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.6.6" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ - "byteorder", - "zerocopy-derive 0.6.6", + "zerocopy-derive 0.7.34", ] [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" dependencies = [ - "zerocopy-derive 0.7.34", + "zerocopy-derive 0.8.14", ] [[package]] name = "zerocopy-derive" -version = "0.6.6" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", @@ -8106,9 +8106,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", diff --git a/Cargo.toml b/Cargo.toml index 637f515d299..034c790fce0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.7" +version = "1.3.8" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index a5ae4ef78c9..b49bd7ed9e9 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.7 + version: 1.3.8 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 25889d5b7d0..51fe2584e6c 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.7 + version: 1.3.8 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 3871449996e..c8dbfec854b 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.7+56 +version: 1.3.8+57 environment: sdk: '^3.1.0' diff --git a/libs/clipboard/Cargo.toml b/libs/clipboard/Cargo.toml index c3673a9bd79..9db2e5a9954 100644 --- a/libs/clipboard/Cargo.toml +++ b/libs/clipboard/Cargo.toml @@ -34,7 +34,6 @@ parking_lot = {version = "0.12"} [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] rand = {version = "0.8", optional = true} -fuser = {version = "0.13", optional = true} libc = {version = "0.2", optional = true} dashmap = {version ="5.5", optional = true} utf16string = {version = "0.2", optional = true} @@ -44,6 +43,7 @@ once_cell = {version = "1.18", optional = true} percent-encoding = {version ="2.3", optional = true} x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true} x11rb = {version = "0.12", features = ["all-extensions"], optional = true} +fuser = {version = "0.15", default-features = false, optional = true} [target.'cfg(target_os = "macos")'.dependencies] cacao = {git="https://github.com/clslaid/cacao", branch = "feat/set-file-urls", optional = true} diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index 6bdd2293aa6..57e6ce6175f 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -1,24 +1,23 @@ -#[allow(dead_code)] -use std::{ - path::PathBuf, - sync::{Arc, Mutex, RwLock}, -}; +use std::sync::{Arc, Mutex, RwLock}; -#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] -use hbb_common::{allow_err, bail}; +#[cfg(target_os = "windows")] +use hbb_common::ResultType; +#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] +use hbb_common::{allow_err, log}; use hbb_common::{ lazy_static, tokio::sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, Mutex as TokioMutex, }, - ResultType, }; use serde_derive::{Deserialize, Serialize}; use thiserror::Error; +#[cfg(target_os = "windows")] pub mod context_send; pub mod platform; +#[cfg(target_os = "windows")] pub use context_send::*; #[cfg(target_os = "windows")] @@ -28,8 +27,10 @@ const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002; #[cfg(target_os = "windows")] const ERR_CODE_SEND_MSG: u32 = 0x00000003; +#[cfg(target_os = "windows")] pub(crate) use platform::create_cliprdr_context; +// to-do: This trait may be removed, because unix file copy paste does not need it. /// Ability to handle Clipboard File from remote rustdesk client /// /// # Note @@ -41,7 +42,6 @@ pub trait CliprdrServiceContext: Send + Sync { fn set_is_stopped(&mut self) -> Result<(), CliprdrError>; /// clear the content on clipboard fn empty_clipboard(&mut self, conn_id: i32) -> Result; - /// run as a server for clipboard RPC fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError>; } @@ -63,9 +63,11 @@ pub enum CliprdrError { #[error("failure to read clipboard")] OpenClipboard, #[error("failure to read file metadata or content")] - FileError { path: PathBuf, err: std::io::Error }, + FileError { path: String, err: std::io::Error }, #[error("invalid request")] InvalidRequest { description: String }, + #[error("common request")] + CommonError { description: String }, #[error("unknown cliprdr error")] Unknown(u32), } @@ -199,37 +201,53 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc ResultType<()> { +pub fn send_data(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> { #[cfg(target_os = "windows")] return send_data_to_channel(conn_id, data); #[cfg(not(target_os = "windows"))] if conn_id == 0 { - send_data_to_all(data); + let _ = send_data_to_all(data); + Ok(()) } else { - send_data_to_channel(conn_id, data); + send_data_to_channel(conn_id, data) } } -#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] + #[inline] -fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> { +#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] +fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> { if let Some(msg_channel) = VEC_MSG_CHANNEL .read() .unwrap() .iter() .find(|x| x.conn_id == conn_id) { - msg_channel.sender.send(data)?; - Ok(()) + msg_channel + .sender + .send(data) + .map_err(|e| CliprdrError::CommonError { + description: e.to_string(), + }) } else { - bail!("conn_id not found"); + Err(CliprdrError::InvalidRequest { + description: "conn_id not found".to_string(), + }) } } +#[inline] #[cfg(target_os = "windows")] pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) { - use hbb_common::log; + // Need more tests to see if it's necessary to handle the error. 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())); @@ -237,14 +255,13 @@ pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) { } } -#[cfg(feature = "unix-file-copy-paste")] #[inline] -fn send_data_to_all(data: ClipboardFile) -> ResultType<()> { +#[cfg(feature = "unix-file-copy-paste")] +fn send_data_to_all(data: ClipboardFile) { // 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/mod.rs b/libs/clipboard/src/platform/mod.rs index 5db27112973..5bf1279cbad 100644 --- a/libs/clipboard/src/platform/mod.rs +++ b/libs/clipboard/src/platform/mod.rs @@ -1,6 +1,3 @@ -#[cfg(any(target_os = "linux", target_os = "macos"))] -use crate::{CliprdrError, CliprdrServiceContext}; - #[cfg(target_os = "windows")] pub mod windows; #[cfg(target_os = "windows")] @@ -16,76 +13,4 @@ pub fn create_cliprdr_context( } #[cfg(feature = "unix-file-copy-paste")] -#[cfg(any(target_os = "linux", target_os = "macos"))] -/// use FUSE for file pasting on these platforms -pub mod fuse; -#[cfg(feature = "unix-file-copy-paste")] -#[cfg(any(target_os = "linux", target_os = "macos"))] pub mod unix; -#[cfg(any(target_os = "linux", target_os = "macos"))] -pub fn create_cliprdr_context( - _enable_files: bool, - _enable_others: bool, - _response_wait_timeout_secs: u32, -) -> crate::ResultType> { - #[cfg(feature = "unix-file-copy-paste")] - { - use std::{fs::Permissions, os::unix::prelude::PermissionsExt}; - - use hbb_common::{config::APP_NAME, log}; - - if !_enable_files { - return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); - } - - let timeout = std::time::Duration::from_secs(_response_wait_timeout_secs as u64); - - let app_name = APP_NAME.read().unwrap().clone(); - - let mnt_path = format!("/tmp/{}/{}", app_name, "cliprdr"); - - // this function must be called after the main IPC is up - std::fs::create_dir(&mnt_path).ok(); - std::fs::set_permissions(&mnt_path, Permissions::from_mode(0o777)).ok(); - - log::info!("clear previously mounted cliprdr FUSE"); - if let Err(e) = std::process::Command::new("umount").arg(&mnt_path).status() { - log::warn!("umount {:?} may fail: {:?}", mnt_path, e); - } - - let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse()?)?; - log::debug!("start cliprdr FUSE"); - unix_ctx.run()?; - - Ok(Box::new(unix_ctx) as Box<_>) - } - - #[cfg(not(feature = "unix-file-copy-paste"))] - 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(()) - } - fn empty_clipboard(&mut self, _conn_id: i32) -> Result { - Ok(true) - } - fn server_clip_file( - &mut self, - _conn_id: i32, - _msg: crate::ClipboardFile, - ) -> Result<(), crate::CliprdrError> { - Ok(()) - } -} - -#[cfg(feature = "unix-file-copy-paste")] -#[cfg(any(target_os = "linux", target_os = "macos"))] -// begin of epoch used by microsoft -// 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00 -const LDAP_EPOCH_DELTA: u64 = 116444772610000000; diff --git a/libs/clipboard/src/platform/unix/filetype.rs b/libs/clipboard/src/platform/unix/filetype.rs new file mode 100644 index 00000000000..6387a3ece3e --- /dev/null +++ b/libs/clipboard/src/platform/unix/filetype.rs @@ -0,0 +1,188 @@ +use super::{FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_UNIX_MODE, LDAP_EPOCH_DELTA}; +use crate::CliprdrError; +use hbb_common::{ + bytes::{Buf, Bytes}, + log, +}; +use std::{ + path::PathBuf, + time::{Duration, SystemTime}, +}; +use utf16string::WStr; + +pub type Inode = u64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileType { + File, + Directory, + // todo: support symlink + Symlink, +} + +/// read only permission +pub const PERM_READ: u16 = 0o444; +/// read and write permission +pub const PERM_RW: u16 = 0o644; +/// only self can read and readonly +pub const PERM_SELF_RO: u16 = 0o400; +/// rwx +pub const PERM_RWX: u16 = 0o755; +/// max length of file name +pub const MAX_NAME_LEN: usize = 255; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FileDescription { + pub conn_id: i32, + pub name: PathBuf, + pub kind: FileType, + pub atime: SystemTime, + pub last_modified: SystemTime, + pub last_metadata_changed: SystemTime, + pub creation_time: SystemTime, + + pub size: u64, + + pub perm: u16, +} + +impl FileDescription { + fn parse_file_descriptor( + bytes: &mut Bytes, + conn_id: i32, + ) -> Result { + let flags = bytes.get_u32_le(); + // skip reserved 32 bytes + bytes.advance(32); + let attributes = bytes.get_u32_le(); + + // in original specification, this is 16 bytes reserved + // we use the last 4 bytes to store the file mode + // skip reserved 12 bytes + bytes.advance(12); + let perm = bytes.get_u32_le() as u16; + + // last write time from 1601-01-01 00:00:00, in 100ns + let last_write_time = bytes.get_u64_le(); + // file size + let file_size_high = bytes.get_u32_le(); + let file_size_low = bytes.get_u32_le(); + // utf16 file name, double \0 terminated, in 520 bytes block + // read with another pointer, and advance the main pointer + let block = bytes.clone(); + bytes.advance(520); + + let block = &block[..520]; + let wstr = WStr::from_utf16le(block).map_err(|e| { + log::error!("cannot convert file descriptor path: {:?}", e); + CliprdrError::ConversionFailure + })?; + + let from_unix = flags & FLAGS_FD_UNIX_MODE != 0; + + let valid_attributes = flags & FLAGS_FD_ATTRIBUTES != 0; + if !valid_attributes { + return Err(CliprdrError::InvalidRequest { + description: "file description must have valid attributes".to_string(), + }); + } + + // todo: check normal, hidden, system, readonly, archive... + let directory = attributes & 0x10 != 0; + let normal = attributes == 0x80; + let hidden = attributes & 0x02 != 0; + let readonly = attributes & 0x01 != 0; + + let perm = if from_unix { + // as is + perm + // cannot set as is... + } else if normal { + PERM_RWX + } else if readonly { + PERM_READ + } else if hidden { + PERM_SELF_RO + } else if directory { + PERM_RWX + } else { + PERM_RW + }; + + let kind = if directory { + FileType::Directory + } else { + FileType::File + }; + + // to-do: use `let valid_size = flags & FLAGS_FD_SIZE != 0;` + // We use `true` to for compatibility with Windows. + // let valid_size = flags & FLAGS_FD_SIZE != 0; + let valid_size = true; + let size = if valid_size { + ((file_size_high as u64) << 32) + file_size_low as u64 + } else { + 0 + }; + + let valid_write_time = flags & FLAGS_FD_LAST_WRITE != 0; + let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA { + let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100; + let last_write_time = Duration::from_nanos(last_write_time); + SystemTime::UNIX_EPOCH + last_write_time + } else { + SystemTime::UNIX_EPOCH + }; + + let name = wstr.to_utf8().replace('\\', "/"); + let name = PathBuf::from(name.trim_end_matches('\0')); + + let desc = FileDescription { + conn_id, + name, + kind, + atime: last_modified, + last_modified, + last_metadata_changed: last_modified, + + creation_time: last_modified, + size, + perm, + }; + + Ok(desc) + } + + /// parse file descriptions from a format data response PDU + /// which containing a CSPTR_FILEDESCRIPTORW indicated format data + pub fn parse_file_descriptors( + file_descriptor_pdu: Vec, + conn_id: i32, + ) -> Result, CliprdrError> { + let mut data = Bytes::from(file_descriptor_pdu); + if data.remaining() < 4 { + return Err(CliprdrError::InvalidRequest { + description: "file descriptor request with infficient length".to_string(), + }); + } + + let count = data.get_u32_le() as usize; + if data.remaining() == 0 && count == 0 { + return Ok(Vec::new()); + } + + if data.remaining() != 592 * count { + return Err(CliprdrError::InvalidRequest { + description: "file descriptor request with invalid length".to_string(), + }); + } + + let mut files = Vec::with_capacity(count); + for _ in 0..count { + let desc = Self::parse_file_descriptor(&mut data, conn_id)?; + files.push(desc); + } + + Ok(files) + } +} diff --git a/libs/clipboard/src/platform/fuse.rs b/libs/clipboard/src/platform/unix/fuse/cs.rs similarity index 82% rename from libs/clipboard/src/platform/fuse.rs rename to libs/clipboard/src/platform/unix/fuse/cs.rs index c5fe60f56e3..0f1cf873936 100644 --- a/libs/clipboard/src/platform/fuse.rs +++ b/libs/clipboard/src/platform/unix/fuse/cs.rs @@ -31,33 +31,29 @@ use std::{ }; use fuser::{ReplyDirectory, FUSE_ROOT_ID}; -use hbb_common::{ - bytes::{Buf, Bytes}, - log, -}; +use hbb_common::log; use parking_lot::{Condvar, Mutex}; -use utf16string::WStr; - -use crate::{send_data, ClipboardFile, CliprdrError}; -use super::LDAP_EPOCH_DELTA; +use crate::{ + platform::unix::{ + filetype::{FileDescription, FileType, Inode, MAX_NAME_LEN, PERM_RWX}, + BLOCK_SIZE, + }, + send_data, ClipboardFile, CliprdrError, +}; /// fuse server ready retry max times const READ_RETRY: i32 = 3; -/// block size for fuse, align to our asynchronic request size over FileContentsRequest. -pub const BLOCK_SIZE: u32 = 4 * 1024 * 1024; - -/// read only permission -const PERM_READ: u16 = 0o444; -/// read and write permission -const PERM_RW: u16 = 0o644; -/// only self can read and readonly -const PERM_SELF_RO: u16 = 0o400; -/// rwx -const PERM_RWX: u16 = 0o755; -/// max length of file name -const MAX_NAME_LEN: usize = 255; +impl From for fuser::FileType { + fn from(value: FileType) -> Self { + match value { + FileType::File => Self::RegularFile, + FileType::Directory => Self::Directory, + FileType::Symlink => Self::Symlink, + } + } +} /// fuse client /// this is a proxy to the fuse server @@ -150,9 +146,15 @@ impl fuser::Filesystem for FuseClient { server.release(req, ino, fh, _flags, _lock_owner, _flush, reply) } - fn getattr(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) { + fn getattr( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: Option, + reply: fuser::ReplyAttr, + ) { let mut server = self.server.lock(); - server.getattr(req, ino, reply) + server.getattr(req, ino, fh, reply) } fn statfs(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyStatfs) { @@ -247,7 +249,6 @@ impl fuser::Filesystem for FuseServer { if parent_entry.attributes.kind != FileType::Directory { log::error!("fuse: parent is not a directory"); - reply.error(libc::ENOTDIR); return; } @@ -480,7 +481,13 @@ impl fuser::Filesystem for FuseServer { reply.ok(); } - fn getattr(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) { + fn getattr( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + _fh: Option, + reply: fuser::ReplyAttr, + ) { let files = &self.files; let Some(entry) = files.get(ino as usize - 1) else { reply.error(libc::ENOENT); @@ -527,14 +534,6 @@ impl FuseServer { size: u32, ) -> Result, std::io::Error> { // todo: async and concurrent read, generate stream_id per request - log::debug!( - "reading {:?} offset {} size {} on stream: {}", - node.name, - offset, - size, - node.stream_id - ); - let cb_requested = unsafe { // convert `size` from u32 to i32 // yet with same bit representation @@ -554,16 +553,14 @@ impl FuseServer { clip_data_id: 0, }; - send_data(node.conn_id, request.clone()); - - log::debug!( - "waiting for read reply for {:?} on stream: {}", - node.name, - node.stream_id - ); + send_data(node.conn_id, request.clone()).map_err(|e| { + log::error!("failed to send file list to channel: {:?}", e); + std::io::Error::new(std::io::ErrorKind::Other, e) + })?; let mut retry_times = 0; + // to-do: more tests needed loop { let reply = self.rx.recv_timeout(self.timeout).map_err(|e| { log::error!("failed to receive file list from channel: {:?}", e); @@ -590,7 +587,10 @@ impl FuseServer { )); } - send_data(node.conn_id, request.clone()); + send_data(node.conn_id, request.clone()).map_err(|e| { + log::error!("failed to send file list to channel: {:?}", e); + std::io::Error::new(std::io::ErrorKind::Other, e) + })?; continue; } return Ok(requested_data); @@ -605,160 +605,6 @@ impl FuseServer { } } } - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FileDescription { - pub conn_id: i32, - pub name: PathBuf, - pub kind: FileType, - pub atime: SystemTime, - pub last_modified: SystemTime, - pub last_metadata_changed: SystemTime, - pub creation_time: SystemTime, - - pub size: u64, - - pub perm: u16, -} - -impl FileDescription { - fn parse_file_descriptor( - bytes: &mut Bytes, - conn_id: i32, - ) -> Result { - let flags = bytes.get_u32_le(); - // skip reserved 32 bytes - bytes.advance(32); - let attributes = bytes.get_u32_le(); - - // in original specification, this is 16 bytes reserved - // we use the last 4 bytes to store the file mode - // skip reserved 12 bytes - bytes.advance(12); - let perm = bytes.get_u32_le() as u16; - - // last write time from 1601-01-01 00:00:00, in 100ns - let last_write_time = bytes.get_u64_le(); - // file size - let file_size_high = bytes.get_u32_le(); - let file_size_low = bytes.get_u32_le(); - // utf16 file name, double \0 terminated, in 520 bytes block - // read with another pointer, and advance the main pointer - let block = bytes.clone(); - bytes.advance(520); - - let block = &block[..520]; - let wstr = WStr::from_utf16le(block).map_err(|e| { - log::error!("cannot convert file descriptor path: {:?}", e); - CliprdrError::ConversionFailure - })?; - - let from_unix = flags & 0x08 != 0; - - let valid_attributes = flags & 0x04 != 0; - if !valid_attributes { - return Err(CliprdrError::InvalidRequest { - description: "file description must have valid attributes".to_string(), - }); - } - - // todo: check normal, hidden, system, readonly, archive... - let directory = attributes & 0x10 != 0; - let normal = attributes == 0x80; - let hidden = attributes & 0x02 != 0; - let readonly = attributes & 0x01 != 0; - - let perm = if from_unix { - // as is - perm - // cannot set as is... - } else if normal { - PERM_RWX - } else if readonly { - PERM_READ - } else if hidden { - PERM_SELF_RO - } else if directory { - PERM_RWX - } else { - PERM_RW - }; - - let kind = if directory { - FileType::Directory - } else { - FileType::File - }; - - let valid_size = flags & 0x40 != 0; - let size = if valid_size { - ((file_size_high as u64) << 32) + file_size_low as u64 - } else { - 0 - }; - - let valid_write_time = flags & 0x20 != 0; - let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA { - let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100; - let last_write_time = Duration::from_nanos(last_write_time); - SystemTime::UNIX_EPOCH + last_write_time - } else { - SystemTime::UNIX_EPOCH - }; - - let name = wstr.to_utf8().replace('\\', "/"); - let name = PathBuf::from(name.trim_end_matches('\0')); - - let desc = FileDescription { - conn_id, - name, - kind, - atime: last_modified, - last_modified, - last_metadata_changed: last_modified, - - creation_time: last_modified, - size, - perm, - }; - - Ok(desc) - } - - /// parse file descriptions from a format data response PDU - /// which containing a CSPTR_FILEDESCRIPTORW indicated format data - pub fn parse_file_descriptors( - file_descriptor_pdu: Vec, - conn_id: i32, - ) -> Result, CliprdrError> { - let mut data = Bytes::from(file_descriptor_pdu); - if data.remaining() < 4 { - return Err(CliprdrError::InvalidRequest { - description: "file descriptor request with infficient length".to_string(), - }); - } - - let count = data.get_u32_le() as usize; - if data.remaining() == 0 && count == 0 { - return Ok(Vec::new()); - } - - if data.remaining() != 592 * count { - return Err(CliprdrError::InvalidRequest { - description: "file descriptor request with invalid length".to_string(), - }); - } - - let mut files = Vec::with_capacity(count); - for _ in 0..count { - let desc = Self::parse_file_descriptor(&mut data, conn_id)?; - files.push(desc); - } - - Ok(files) - } -} - /// a node in the FUSE file tree #[derive(Debug)] struct FuseNode { @@ -881,7 +727,7 @@ impl FuseNode { format!("invalid file name {}", file.name.display()), ); CliprdrError::FileError { - path: file.name.clone(), + path: file.name.to_string_lossy().to_string(), err, } })?; @@ -902,26 +748,6 @@ impl FuseNode { } } -pub type Inode = u64; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FileType { - File, - Directory, - // todo: support symlink - Symlink, -} - -impl From for fuser::FileType { - fn from(value: FileType) -> Self { - match value { - FileType::File => Self::RegularFile, - FileType::Directory => Self::Directory, - FileType::Symlink => Self::Symlink, - } - } -} - #[derive(Debug, Clone)] pub struct InodeAttributes { inode: Inode, @@ -1064,8 +890,6 @@ impl FileHandles { #[cfg(test)] mod fuse_test { - use std::str::FromStr; - use super::*; // todo: more tests needed! diff --git a/libs/clipboard/src/platform/unix/fuse/mod.rs b/libs/clipboard/src/platform/unix/fuse/mod.rs new file mode 100644 index 00000000000..df743004fb2 --- /dev/null +++ b/libs/clipboard/src/platform/unix/fuse/mod.rs @@ -0,0 +1,225 @@ +mod cs; + +use super::filetype::FileDescription; +use crate::{ClipboardFile, CliprdrError}; +use cs::FuseServer; +use fuser::MountOption; +use hbb_common::{config::APP_NAME, log}; +use parking_lot::Mutex; +use std::{ + path::PathBuf, + sync::{mpsc::Sender, Arc}, + time::Duration, +}; + +lazy_static::lazy_static! { + static ref FUSE_MOUNT_POINT_CLIENT: Arc = { + let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-client"); + // No need to run `canonicalize()` here. + Arc::new(mnt_path) + }; + + static ref FUSE_MOUNT_POINT_SERVER: Arc = { + let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-server"); + // No need to run `canonicalize()` here. + Arc::new(mnt_path) + }; + + static ref FUSE_CONTEXT_CLIENT: Arc>> = Arc::new(Mutex::new(None)); + static ref FUSE_CONTEXT_SERVER: Arc>> = Arc::new(Mutex::new(None)); +} + +static FUSE_TIMEOUT: Duration = Duration::from_secs(3); + +pub fn get_exclude_paths(is_client: bool) -> Arc { + if is_client { + FUSE_MOUNT_POINT_CLIENT.clone() + } else { + FUSE_MOUNT_POINT_SERVER.clone() + } +} + +pub fn is_fuse_context_inited(is_client: bool) -> bool { + if is_client { + FUSE_CONTEXT_CLIENT.lock().is_some() + } else { + FUSE_CONTEXT_SERVER.lock().is_some() + } +} + +pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> { + let mut fuse_context_lock = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + if fuse_context_lock.is_some() { + return Ok(()); + } + let mount_point = if is_client { + FUSE_MOUNT_POINT_CLIENT.clone() + } else { + FUSE_MOUNT_POINT_SERVER.clone() + }; + + let mount_point = std::path::PathBuf::from(&*mount_point); + let (server, tx) = FuseServer::new(FUSE_TIMEOUT); + let server = Arc::new(Mutex::new(server)); + + prepare_fuse_mount_point(&mount_point); + let mnt_opts = [ + MountOption::FSName("rustdesk-cliprdr-fs".to_string()), + MountOption::NoAtime, + MountOption::RO, + ]; + log::info!("mounting clipboard FUSE to {}", mount_point.display()); + // to-do: ignore the error if the mount point is already mounted + // Because the sciter version uses separate processes as the controlling side. + let session = fuser::spawn_mount2( + FuseServer::client(server.clone()), + mount_point.clone(), + &mnt_opts, + ) + .map_err(|e| { + log::error!("failed to mount cliprdr fuse: {:?}", e); + CliprdrError::CliprdrInit + })?; + let session = Mutex::new(Some(session)); + + let ctx = FuseContext { + server, + tx, + mount_point, + session, + conn_id: 0, + }; + *fuse_context_lock = Some(ctx); + Ok(()) +} + +pub fn uninit_fuse_context(is_client: bool) { + uninit_fuse_context_(is_client) +} + +pub fn format_data_response_to_urls( + is_client: bool, + format_data: Vec, + conn_id: i32, +) -> Result, CliprdrError> { + let mut ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_mut() + .ok_or(CliprdrError::CliprdrInit)? + .format_data_response_to_urls(format_data, conn_id) +} + +pub fn handle_file_content_response( + is_client: bool, + clip: ClipboardFile, +) -> Result<(), CliprdrError> { + // we don't know its corresponding request, no resend can be performed + let ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_ref() + .ok_or(CliprdrError::CliprdrInit)? + .tx + .send(clip) + .map_err(|e| { + log::error!("failed to send file contents response to fuse: {:?}", e); + CliprdrError::ClipboardInternalError + })?; + Ok(()) +} + +pub fn empty_local_files(is_client: bool, conn_id: i32) -> bool { + let ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_ref() + .map(|c| c.empty_local_files(conn_id)) + .unwrap_or(false) +} + +struct FuseContext { + server: Arc>, + tx: Sender, + mount_point: PathBuf, + // stores fuse background session handle + session: Mutex>, + // Indicates the connection ID of that set the clipboard content + conn_id: i32, +} + +// this function must be called after the main IPC is up +fn prepare_fuse_mount_point(mount_point: &PathBuf) { + use std::{ + fs::{self, Permissions}, + os::unix::prelude::PermissionsExt, + }; + + fs::create_dir(mount_point).ok(); + fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok(); + + if let Err(e) = std::process::Command::new("umount") + .arg(mount_point) + .status() + { + log::warn!("umount {:?} may fail: {:?}", mount_point, e); + } +} + +fn uninit_fuse_context_(is_client: bool) { + if is_client { + let _ = FUSE_CONTEXT_CLIENT.lock().take(); + } else { + let _ = FUSE_CONTEXT_SERVER.lock().take(); + } +} + +impl Drop for FuseContext { + fn drop(&mut self) { + self.session.lock().take().map(|s| s.join()); + log::info!("unmounting clipboard FUSE from {}", self.mount_point.display()); + } +} + +impl FuseContext { + pub fn empty_local_files(&self, conn_id: i32) -> bool { + if conn_id != 0 && self.conn_id != conn_id { + return false; + } + let mut fuse_guard = self.server.lock(); + let _ = fuse_guard.load_file_list(vec![]); + true + } + + pub fn format_data_response_to_urls( + &mut self, + format_data: Vec, + conn_id: i32, + ) -> Result, CliprdrError> { + let files = FileDescription::parse_file_descriptors(format_data, conn_id)?; + + let paths = { + let mut fuse_guard = self.server.lock(); + fuse_guard.load_file_list(files)?; + self.conn_id = conn_id; + + fuse_guard.list_root() + }; + + let prefix = self.mount_point.clone(); + Ok(paths + .into_iter() + .map(|p| prefix.join(p).to_string_lossy().to_string()) + .collect()) + } +} diff --git a/libs/clipboard/src/platform/unix/local_file.rs b/libs/clipboard/src/platform/unix/local_file.rs index b609b8cc79e..11d62cad8ee 100644 --- a/libs/clipboard/src/platform/unix/local_file.rs +++ b/libs/clipboard/src/platform/unix/local_file.rs @@ -1,3 +1,15 @@ +use super::{BLOCK_SIZE, LDAP_EPOCH_DELTA}; +use crate::{ + platform::unix::{ + FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_PROGRESSUI, FLAGS_FD_SIZE, + FLAGS_FD_UNIX_MODE, + }, + CliprdrError, +}; +use hbb_common::{ + bytes::{BufMut, BytesMut}, + log, +}; use std::{ collections::HashSet, fs::File, @@ -7,32 +19,11 @@ use std::{ sync::atomic::{AtomicU64, Ordering}, time::SystemTime, }; - -use hbb_common::{ - bytes::{BufMut, BytesMut}, - log, -}; use utf16string::WString; -use crate::{ - platform::{fuse::BLOCK_SIZE, LDAP_EPOCH_DELTA}, - CliprdrError, -}; - -/// has valid file attributes -const FLAGS_FD_ATTRIBUTES: u32 = 0x04; -/// has valid file size -const FLAGS_FD_SIZE: u32 = 0x40; -/// has valid last write time -const FLAGS_FD_LAST_WRITE: u32 = 0x20; -/// show progress -const FLAGS_FD_PROGRESSUI: u32 = 0x4000; -/// transferred from unix, contains file mode -/// P.S. this flag is not used in windows -const FLAGS_FD_UNIX_MODE: u32 = 0x08; - #[derive(Debug)] pub(super) struct LocalFile { + pub relative_root: PathBuf, pub path: PathBuf, pub handle: Option>, @@ -51,9 +42,9 @@ pub(super) struct LocalFile { } impl LocalFile { - pub fn try_open(path: &Path) -> Result { + pub fn try_open(relative_root: &Path, path: &Path) -> Result { let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { - path: path.clone(), + path: path.to_string_lossy().to_string(), err: e, })?; let size = mt.len() as u64; @@ -79,7 +70,8 @@ impl LocalFile { Ok(Self { name, - path: path.clone(), + relative_root: relative_root.to_path_buf(), + path: path.to_path_buf(), handle, offset, size, @@ -121,7 +113,12 @@ impl LocalFile { let size_high = (self.size >> 32) as u32; let size_low = (self.size & (u32::MAX as u64)) as u32; - let path = self.path.to_string_lossy().to_string(); + let path = self + .path + .strip_prefix(&self.relative_root) + .unwrap_or(&self.path) + .to_string_lossy() + .into_owned(); let wstr: WString = WString::from(&path); let name = wstr.as_bytes(); @@ -172,12 +169,12 @@ impl LocalFile { pub fn load_handle(&mut self) -> Result<(), CliprdrError> { if !self.is_dir && self.handle.is_none() { let handle = std::fs::File::open(&self.path).map_err(|e| CliprdrError::FileError { - path: self.path.clone(), + path: self.path.to_string_lossy().to_string(), err: e, })?; let mut reader = BufReader::with_capacity(BLOCK_SIZE as usize * 2, handle); reader.fill_buf().map_err(|e| CliprdrError::FileError { - path: self.path.clone(), + path: self.path.to_string_lossy().to_string(), err: e, })?; self.handle = Some(reader); @@ -188,20 +185,25 @@ impl LocalFile { pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> { self.load_handle()?; - let handle = self.handle.as_mut()?; + let Some(handle) = self.handle.as_mut() else { + return Err(CliprdrError::FileError { + path: self.path.to_string_lossy().to_string(), + err: std::io::Error::new(std::io::ErrorKind::NotFound, "file handle not found"), + }); + }; if offset != self.offset.load(Ordering::Relaxed) { handle .seek(std::io::SeekFrom::Start(offset)) .map_err(|e| CliprdrError::FileError { - path: self.path.clone(), + path: self.path.to_string_lossy().to_string(), err: e, })?; } handle .read_exact(buf) .map_err(|e| CliprdrError::FileError { - path: self.path.clone(), + path: self.path.to_string_lossy().to_string(), err: e, })?; let new_offset = offset + (buf.len() as u64); @@ -219,6 +221,7 @@ impl LocalFile { pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, CliprdrError> { fn constr_file_lst( + relative_root: &Path, path: &Path, file_list: &mut Vec, visited: &mut HashSet, @@ -227,22 +230,28 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, C if visited.contains(path) { return Ok(()); } - visited.insert(path.clone()); + visited.insert(path.to_path_buf()); - let local_file = LocalFile::try_open(path)?; + let local_file = LocalFile::try_open(relative_root, path)?; file_list.push(local_file); let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { - path: path.clone(), + path: path.to_string_lossy().to_string(), err: e, })?; if mt.is_dir() { - let dir = std::fs::read_dir(path)?; + let dir = std::fs::read_dir(path).map_err(|e| CliprdrError::FileError { + path: path.to_string_lossy().to_string(), + err: e, + })?; for entry in dir { - let entry = entry?; + let entry = entry.map_err(|e| CliprdrError::FileError { + path: path.to_string_lossy().to_string(), + err: e, + })?; let path = entry.path(); - constr_file_lst(&path, file_list, visited)?; + constr_file_lst(relative_root, &path, file_list, visited)?; } } Ok(()) @@ -251,8 +260,18 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, C let mut file_list = Vec::new(); let mut visited = HashSet::new(); + let relative_root = paths + .first() + .ok_or(CliprdrError::InvalidRequest { + description: "empty file list".to_string(), + })? + .parent() + .ok_or(CliprdrError::InvalidRequest { + description: "empty parent".to_string(), + })? + .to_path_buf(); for path in paths { - constr_file_lst(path, &mut file_list, &mut visited)?; + constr_file_lst(&relative_root, path, &mut file_list, &mut visited)?; } Ok(file_list) } @@ -263,7 +282,7 @@ mod file_list_test { use hbb_common::bytes::{BufMut, BytesMut}; - use crate::{platform::fuse::FileDescription, CliprdrError}; + use crate::{platform::unix::filetype::FileDescription, CliprdrError}; use super::LocalFile; @@ -277,6 +296,7 @@ mod file_list_test { #[inline] fn generate_file(path: &str, name: &str, is_dir: bool) -> LocalFile { LocalFile { + relative_root: PathBuf::from("."), path: PathBuf::from(path), handle: None, name: name.to_string(), diff --git a/libs/clipboard/src/platform/unix/mod.rs b/libs/clipboard/src/platform/unix/mod.rs index 34021d6bf20..7e7aeccb150 100644 --- a/libs/clipboard/src/platform/unix/mod.rs +++ b/libs/clipboard/src/platform/unix/mod.rs @@ -1,48 +1,38 @@ -use std::{ - path::{Path, PathBuf}, - sync::{mpsc::Sender, Arc}, - time::Duration, -}; - use dashmap::DashMap; -use fuser::MountOption; -use hbb_common::{ - bytes::{BufMut, BytesMut}, - log, -}; use lazy_static::lazy_static; -use parking_lot::Mutex; - -use crate::{ - platform::{fuse::FileDescription, unix::local_file::construct_file_list}, - send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, -}; - -use self::local_file::LocalFile; -#[cfg(target_os = "linux")] -use self::url::{encode_path_to_uri, parse_plain_uri_list}; - -use super::fuse::FuseServer; +mod filetype; +/// use FUSE for file pasting on these platforms #[cfg(target_os = "linux")] -/// clipboard implementation of x11 -pub mod x11; - -#[cfg(target_os = "macos")] -/// clipboard implementation of macos -pub mod ns_clipboard; - +pub mod fuse; pub mod local_file; - -#[cfg(target_os = "linux")] -pub mod url; +pub mod serv_files; + +/// has valid file attributes +pub const FLAGS_FD_ATTRIBUTES: u32 = 0x04; +/// has valid file size +pub const FLAGS_FD_SIZE: u32 = 0x40; +/// has valid last write time +pub const FLAGS_FD_LAST_WRITE: u32 = 0x20; +/// show progress +pub const FLAGS_FD_PROGRESSUI: u32 = 0x4000; +/// transferred from unix, contains file mode +/// P.S. this flag is not used in windows +pub const FLAGS_FD_UNIX_MODE: u32 = 0x08; // not actual format id, just a placeholder -const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334; -const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW"; +pub const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334; +pub const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW"; // not actual format id, just a placeholder -const FILECONTENTS_FORMAT_ID: i32 = 49267; -const FILECONTENTS_FORMAT_NAME: &str = "FileContents"; +pub const FILECONTENTS_FORMAT_ID: i32 = 49267; +pub const FILECONTENTS_FORMAT_NAME: &str = "FileContents"; + +/// block size for fuse, align to our asynchronic request size over FileContentsRequest. +pub(crate) const BLOCK_SIZE: u32 = 4 * 1024 * 1024; + +// begin of epoch used by microsoft +// 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00 +const LDAP_EPOCH_DELTA: u64 = 116444772610000000; lazy_static! { static ref REMOTE_FORMAT_MAP: DashMap = DashMap::from_iter( @@ -58,541 +48,7 @@ lazy_static! { ); } -fn get_local_format(remote_id: i32) -> Option { +#[inline] +pub fn get_local_format(remote_id: i32) -> Option { REMOTE_FORMAT_MAP.get(&remote_id).map(|s| s.clone()) } - -fn add_remote_format(local_name: &str, remote_id: i32) { - REMOTE_FORMAT_MAP.insert(remote_id, local_name.to_string()); -} - -trait SysClipboard: Send + Sync { - fn start(&self); - - fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>; - fn get_file_list(&self) -> Vec; -} - -#[cfg(target_os = "linux")] -fn get_sys_clipboard(ignore_path: &Path) -> Result, CliprdrError> { - #[cfg(feature = "wayland")] - { - unimplemented!() - } - #[cfg(not(feature = "wayland"))] - { - use x11::*; - let x11_clip = X11Clipboard::new(ignore_path)?; - Ok(Box::new(x11_clip) as Box<_>) - } -} - -#[cfg(target_os = "macos")] -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<_>) -} - -#[derive(Debug)] -enum FileContentsRequest { - Size { - stream_id: i32, - file_idx: usize, - }, - - Range { - stream_id: i32, - file_idx: usize, - offset: u64, - length: u64, - }, -} - -pub struct ClipboardContext { - pub fuse_mount_point: PathBuf, - /// stores fuse background session handle - fuse_handle: Mutex>, - - /// a sender of clipboard file contents pdu to fuse server - fuse_tx: Sender, - fuse_server: Arc>, - - clipboard: Arc, - local_files: Mutex>, -} - -impl ClipboardContext { - pub fn new(timeout: Duration, mount_path: PathBuf) -> Result { - // assert mount path exists - let fuse_mount_point = mount_path.canonicalize().map_err(|e| { - log::error!("failed to canonicalize mount path: {:?}", e); - CliprdrError::CliprdrInit - })?; - - let (fuse_server, fuse_tx) = FuseServer::new(timeout); - - let fuse_server = Arc::new(Mutex::new(fuse_server)); - - let clipboard = get_sys_clipboard(&fuse_mount_point)?; - let clipboard = Arc::from(clipboard) as Arc<_>; - let local_files = Mutex::new(vec![]); - - Ok(Self { - fuse_mount_point, - fuse_server, - fuse_tx, - fuse_handle: Mutex::new(None), - clipboard, - local_files, - }) - } - - pub fn run(&self) -> Result<(), CliprdrError> { - if !self.is_stopped() { - return Ok(()); - } - - let mut fuse_handle = self.fuse_handle.lock(); - - let mount_path = &self.fuse_mount_point; - - let mnt_opts = [ - MountOption::FSName("rustdesk-cliprdr-fs".to_string()), - MountOption::NoAtime, - MountOption::RO, - ]; - log::info!( - "mounting clipboard FUSE to {}", - self.fuse_mount_point.display() - ); - - let new_handle = fuser::spawn_mount2( - FuseServer::client(self.fuse_server.clone()), - mount_path, - &mnt_opts, - ) - .map_err(|e| { - log::error!("failed to mount cliprdr fuse: {:?}", e); - CliprdrError::CliprdrInit - })?; - *fuse_handle = Some(new_handle); - - let clipboard = self.clipboard.clone(); - - std::thread::spawn(move || { - log::debug!("start listening clipboard"); - clipboard.start(); - }); - - Ok(()) - } - - /// set clipboard data from file list - pub fn set_clipboard(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { - let prefix = self.fuse_mount_point.clone(); - let paths: Vec = paths.iter().cloned().map(|p| prefix.join(p)).collect(); - log::debug!("setting clipboard with paths: {:?}", paths); - self.clipboard.set_file_list(&paths)?; - log::debug!("clipboard set, paths: {:?}", paths); - Ok(()) - } - - fn serve_file_contents( - &self, - conn_id: i32, - request: FileContentsRequest, - ) -> Result<(), CliprdrError> { - let mut file_list = self.local_files.lock(); - - let (file_idx, file_contents_resp) = match request { - FileContentsRequest::Size { - stream_id, - file_idx, - } => { - log::debug!("file contents (size) requested from conn: {}", conn_id); - let Some(file) = file_list.get(file_idx) else { - log::error!( - "invalid file index {} requested from conn: {}", - file_idx, - conn_id - ); - resp_file_contents_fail(conn_id, stream_id); - - return Err(CliprdrError::InvalidRequest { - description: format!( - "invalid file index {} requested from conn: {}", - file_idx, conn_id - ), - }); - }; - - log::debug!( - "conn {} requested file-{}: {}", - conn_id, - file_idx, - file.name - ); - - let size = file.size; - ( - file_idx, - ClipboardFile::FileContentsResponse { - msg_flags: 0x1, - stream_id, - requested_data: size.to_le_bytes().to_vec(), - }, - ) - } - FileContentsRequest::Range { - stream_id, - file_idx, - offset, - length, - } => { - log::debug!( - "file contents (range from {} length {}) request from conn: {}", - offset, - length, - conn_id - ); - let Some(file) = file_list.get_mut(file_idx) else { - log::error!( - "invalid file index {} requested from conn: {}", - file_idx, - conn_id - ); - resp_file_contents_fail(conn_id, stream_id); - return Err(CliprdrError::InvalidRequest { - description: format!( - "invalid file index {} requested from conn: {}", - file_idx, conn_id - ), - }); - }; - log::debug!( - "conn {} requested file-{}: {}", - conn_id, - file_idx, - file.name - ); - - if offset > file.size { - log::error!("invalid reading offset requested from conn: {}", conn_id); - resp_file_contents_fail(conn_id, stream_id); - - return Err(CliprdrError::InvalidRequest { - description: format!( - "invalid reading offset requested from conn: {}", - conn_id - ), - }); - } - let read_size = if offset + length > file.size { - file.size - offset - } else { - length - }; - - let mut buf = vec![0u8; read_size as usize]; - - file.read_exact_at(&mut buf, offset)?; - - ( - file_idx, - ClipboardFile::FileContentsResponse { - msg_flags: 0x1, - stream_id, - requested_data: buf, - }, - ) - } - }; - - send_data(conn_id, file_contents_resp); - log::debug!("file contents sent to conn: {}", conn_id); - // hot reload next file - for next_file in file_list.iter_mut().skip(file_idx + 1) { - if !next_file.is_dir { - next_file.load_handle()?; - break; - } - } - Ok(()) - } -} - -fn resp_file_contents_fail(conn_id: i32, stream_id: i32) { - let resp = ClipboardFile::FileContentsResponse { - msg_flags: 0x2, - stream_id, - requested_data: vec![], - }; - send_data(conn_id, resp) -} - -impl ClipboardContext { - pub fn is_stopped(&self) -> bool { - self.fuse_handle.lock().is_none() - } - - pub fn sync_local_files(&self) -> Result<(), CliprdrError> { - let mut local_files = self.local_files.lock(); - let clipboard_files = self.clipboard.get_file_list(); - let local_file_list: Vec = local_files.iter().map(|f| f.path.clone()).collect(); - if local_file_list == clipboard_files { - return Ok(()); - } - let new_files = construct_file_list(&clipboard_files)?; - *local_files = new_files; - Ok(()) - } - - pub fn serve(&self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { - log::debug!("serve clipboard file from conn: {}", conn_id); - if self.is_stopped() { - log::debug!("cliprdr stopped, restart it"); - self.run()?; - } - match msg { - ClipboardFile::NotifyCallback { .. } => { - unreachable!() - } - ClipboardFile::MonitorReady => { - log::debug!("server_monitor_ready called"); - - self.send_file_list(conn_id)?; - - Ok(()) - } - - ClipboardFile::FormatList { format_list } => { - log::debug!("server_format_list called"); - // filter out "FileGroupDescriptorW" and "FileContents" - let fmt_lst: Vec<(i32, String)> = format_list - .into_iter() - .filter(|(_, name)| { - name == FILEDESCRIPTORW_FORMAT_NAME || name == FILECONTENTS_FORMAT_NAME - }) - .collect(); - if fmt_lst.len() != 2 { - log::debug!("no supported formats"); - return Ok(()); - } - log::debug!("supported formats: {:?}", fmt_lst); - let file_contents_id = fmt_lst - .iter() - .find(|(_, name)| name == FILECONTENTS_FORMAT_NAME) - .map(|(id, _)| *id)?; - let file_descriptor_id = fmt_lst - .iter() - .find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME) - .map(|(id, _)| *id)?; - - add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id); - add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id); - - // sync file system from peer - let data = ClipboardFile::FormatDataRequest { - requested_format_id: file_descriptor_id, - }; - send_data(conn_id, data); - - Ok(()) - } - ClipboardFile::FormatListResponse { msg_flags } => { - log::debug!("server_format_list_response called"); - if msg_flags != 0x1 { - send_format_list(conn_id) - } else { - Ok(()) - } - } - ClipboardFile::FormatDataRequest { - requested_format_id, - } => { - log::debug!("server_format_data_request called"); - let Some(format) = get_local_format(requested_format_id) else { - log::error!( - "got unsupported format data request: id={} from conn={}", - requested_format_id, - conn_id - ); - resp_format_data_failure(conn_id); - return Ok(()); - }; - - if format == FILEDESCRIPTORW_FORMAT_NAME { - self.send_file_list(conn_id)?; - } else if format == FILECONTENTS_FORMAT_NAME { - log::error!( - "try to read file contents with FormatDataRequest from conn={}", - conn_id - ); - resp_format_data_failure(conn_id); - } else { - log::error!( - "got unsupported format data request: id={} from conn={}", - requested_format_id, - conn_id - ); - resp_format_data_failure(conn_id); - } - Ok(()) - } - ClipboardFile::FormatDataResponse { - msg_flags, - format_data, - } => { - log::debug!( - "server_format_data_response called, msg_flags={}", - msg_flags - ); - - if msg_flags != 0x1 { - resp_format_data_failure(conn_id); - return Ok(()); - } - - log::debug!("parsing file descriptors"); - // this must be a file descriptor format data - let files = FileDescription::parse_file_descriptors(format_data, conn_id)?; - - let paths = { - let mut fuse_guard = self.fuse_server.lock(); - fuse_guard.load_file_list(files)?; - - fuse_guard.list_root() - }; - - log::debug!("load file list: {:?}", paths); - self.set_clipboard(&paths)?; - Ok(()) - } - ClipboardFile::FileContentsResponse { .. } => { - log::debug!("server_file_contents_response called"); - // we don't know its corresponding request, no resend can be performed - self.fuse_tx.send(msg).map_err(|e| { - log::error!("failed to send file contents response to fuse: {:?}", e); - CliprdrError::ClipboardInternalError - })?; - Ok(()) - } - ClipboardFile::FileContentsRequest { - stream_id, - list_index, - dw_flags, - n_position_low, - n_position_high, - cb_requested, - .. - } => { - log::debug!("server_file_contents_request called"); - let fcr = if dw_flags == 0x1 { - FileContentsRequest::Size { - stream_id, - file_idx: list_index as usize, - } - } else if dw_flags == 0x2 { - let offset = (n_position_high as u64) << 32 | n_position_low as u64; - let length = cb_requested as u64; - - FileContentsRequest::Range { - stream_id, - file_idx: list_index as usize, - offset, - length, - } - } else { - log::error!("got invalid FileContentsRequest from conn={}", conn_id); - resp_file_contents_fail(conn_id, stream_id); - return Ok(()); - }; - - self.serve_file_contents(conn_id, fcr) - } - } - } - - fn send_file_list(&self, conn_id: i32) -> Result<(), CliprdrError> { - self.sync_local_files()?; - - let file_list = self.local_files.lock(); - send_file_list(&*file_list, conn_id) - } -} - -impl CliprdrServiceContext for ClipboardContext { - fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { - // unmount the fuse - if let Some(fuse_handle) = self.fuse_handle.lock().take() { - fuse_handle.join(); - } - // we don't stop the clipboard, keep listening in case of restart - Ok(()) - } - - fn empty_clipboard(&mut self, _conn_id: i32) -> Result { - self.clipboard.set_file_list(&[])?; - Ok(true) - } - - fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { - self.serve(conn_id, msg) - } -} - -fn resp_format_data_failure(conn_id: i32) { - let data = ClipboardFile::FormatDataResponse { - msg_flags: 0x2, - format_data: vec![], - }; - send_data(conn_id, data) -} - -fn send_format_list(conn_id: i32) -> Result<(), CliprdrError> { - log::debug!("send format list to remote, conn={}", conn_id); - let fd_format_name = get_local_format(FILEDESCRIPTOR_FORMAT_ID) - .unwrap_or(FILEDESCRIPTORW_FORMAT_NAME.to_string()); - let fc_format_name = - get_local_format(FILECONTENTS_FORMAT_ID).unwrap_or(FILECONTENTS_FORMAT_NAME.to_string()); - let format_list = ClipboardFile::FormatList { - format_list: vec![ - (FILEDESCRIPTOR_FORMAT_ID, fd_format_name), - (FILECONTENTS_FORMAT_ID, fc_format_name), - ], - }; - - send_data(conn_id, format_list); - log::debug!("format list to remote dispatched, conn={}", conn_id); - Ok(()) -} - -fn build_file_list_pdu(files: &[LocalFile]) -> Vec { - let mut data = BytesMut::with_capacity(4 + 592 * files.len()); - data.put_u32_le(files.len() as u32); - for file in files.iter() { - data.put(file.as_bin().as_slice()); - } - - data.to_vec() -} - -fn send_file_list(files: &[LocalFile], conn_id: i32) -> Result<(), CliprdrError> { - log::debug!( - "send file list to remote, conn={}, list={:?}", - conn_id, - files.iter().map(|f| f.path.display()).collect::>() - ); - - let format_data = build_file_list_pdu(files); - - send_data( - conn_id, - ClipboardFile::FormatDataResponse { - msg_flags: 1, - format_data, - }, - ); - Ok(()) -} diff --git a/libs/clipboard/src/platform/unix/ns_clipboard.rs b/libs/clipboard/src/platform/unix/ns_clipboard.rs deleted file mode 100644 index a9112fe6259..00000000000 --- a/libs/clipboard/src/platform/unix/ns_clipboard.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::{ - collections::BTreeSet, - path::{Path, PathBuf}, -}; - -use cacao::pasteboard::{Pasteboard, PasteboardName}; -use hbb_common::log; -use parking_lot::Mutex; - -use crate::{platform::unix::send_format_list, CliprdrError}; - -use super::SysClipboard; - -#[inline] -fn wait_file_list() -> Option> { - let pb = Pasteboard::named(PasteboardName::General); - pb.get_file_urls() - .ok() - .map(|v| v.into_iter().map(|nsurl| nsurl.pathbuf()).collect()) -} - -#[inline] -fn set_file_list(file_list: &[PathBuf]) -> Result<(), CliprdrError> { - let pb = Pasteboard::named(PasteboardName::General); - pb.set_files(file_list.to_vec()) - .map_err(|_| CliprdrError::ClipboardInternalError) -} - -pub struct NsPasteboard { - ignore_path: PathBuf, - - former_file_list: Mutex>, -} - -impl NsPasteboard { - pub fn new(ignore_path: &Path) -> Result { - Ok(Self { - ignore_path: ignore_path.to_owned(), - former_file_list: Mutex::new(vec![]), - }) - } -} - -impl SysClipboard for NsPasteboard { - fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { - *self.former_file_list.lock() = paths.to_vec(); - set_file_list(paths) - } - - fn start(&self) { - { - *self.former_file_list.lock() = vec![]; - } - - loop { - let file_list = match wait_file_list() { - Some(v) => v, - None => { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - }; - - let filtered = file_list - .into_iter() - .filter(|pb| !pb.starts_with(&self.ignore_path)) - .collect::>(); - - if filtered.is_empty() { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - - { - let mut former = self.former_file_list.lock(); - - let filtered_st: BTreeSet<_> = filtered.iter().collect(); - let former_st = former.iter().collect::>(); - if filtered_st == former_st { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - - *former = filtered; - } - - if let Err(e) = send_format_list(0) { - log::warn!("failed to send format list: {}", e); - break; - } - - std::thread::sleep(std::time::Duration::from_millis(100)); - } - log::debug!("stop listening file related atoms on clipboard"); - } - - fn get_file_list(&self) -> Vec { - self.former_file_list.lock().clone() - } -} diff --git a/libs/clipboard/src/platform/unix/serv_files.rs b/libs/clipboard/src/platform/unix/serv_files.rs new file mode 100644 index 00000000000..a401e0b5cac --- /dev/null +++ b/libs/clipboard/src/platform/unix/serv_files.rs @@ -0,0 +1,231 @@ +use super::local_file::LocalFile; +use crate::{platform::unix::local_file::construct_file_list, ClipboardFile, CliprdrError}; +use hbb_common::{ + bytes::{BufMut, BytesMut}, + log, +}; +use parking_lot::Mutex; +use std::{path::PathBuf, sync::Arc}; + +lazy_static::lazy_static! { + // local files are cached, this value should not be changed when copying files + // Because `CliprdrFileContentsRequest` only contains the index of the file in the list. + // We need to keep the file list in the same order as the remote side. + // We may add a `FileId` field to `CliprdrFileContentsRequest` in the future. + static ref CLIP_FILES: Arc> = Default::default(); +} + +#[derive(Debug)] +enum FileContentsRequest { + Size { + stream_id: i32, + file_idx: usize, + }, + + Range { + stream_id: i32, + file_idx: usize, + offset: u64, + length: u64, + }, +} + +#[derive(Default)] +struct ClipFiles { + files: Vec, + file_list: Vec, + files_pdu: Vec, +} + +impl ClipFiles { + fn clear(&mut self) { + self.files.clear(); + self.file_list.clear(); + self.files_pdu.clear(); + } + + fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> { + let clipboard_paths = clipboard_files + .iter() + .map(|s| PathBuf::from(s)) + .collect::>(); + self.file_list = construct_file_list(&clipboard_paths)?; + self.files = clipboard_files.to_vec(); + Ok(()) + } + + fn build_file_list_pdu(&mut self) { + let mut data = BytesMut::with_capacity(4 + 592 * self.file_list.len()); + data.put_u32_le(self.file_list.len() as u32); + for file in self.file_list.iter() { + data.put(file.as_bin().as_slice()); + } + self.files_pdu = data.to_vec() + } + + fn serve_file_contents( + &mut self, + conn_id: i32, + request: FileContentsRequest, + ) -> Result { + let (file_idx, file_contents_resp) = match request { + FileContentsRequest::Size { + stream_id, + file_idx, + } => { + log::debug!("file contents (size) requested from conn: {}", conn_id); + let Some(file) = self.file_list.get(file_idx) else { + log::error!( + "invalid file index {} requested from conn: {}", + file_idx, + conn_id + ); + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid file index {} requested from conn: {}", + file_idx, conn_id + ), + }); + }; + + log::debug!( + "conn {} requested file-{}: {}", + conn_id, + file_idx, + file.name + ); + + let size = file.size; + ( + file_idx, + ClipboardFile::FileContentsResponse { + msg_flags: 0x1, + stream_id, + requested_data: size.to_le_bytes().to_vec(), + }, + ) + } + FileContentsRequest::Range { + stream_id, + file_idx, + offset, + length, + } => { + log::debug!( + "file contents (range from {} length {}) request from conn: {}", + offset, + length, + conn_id + ); + let Some(file) = self.file_list.get_mut(file_idx) else { + log::error!( + "invalid file index {} requested from conn: {}", + file_idx, + conn_id + ); + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid file index {} requested from conn: {}", + file_idx, conn_id + ), + }); + }; + log::debug!( + "conn {} requested file-{}: {}", + conn_id, + file_idx, + file.name + ); + + if offset > file.size { + log::error!("invalid reading offset requested from conn: {}", conn_id); + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid reading offset requested from conn: {}", + conn_id + ), + }); + } + let read_size = if offset + length > file.size { + file.size - offset + } else { + length + }; + + let mut buf = vec![0u8; read_size as usize]; + + file.read_exact_at(&mut buf, offset)?; + + ( + file_idx, + ClipboardFile::FileContentsResponse { + msg_flags: 0x1, + stream_id, + requested_data: buf, + }, + ) + } + }; + + log::debug!("file contents sent to conn: {}", conn_id); + // hot reload next file + for next_file in self.file_list.iter_mut().skip(file_idx + 1) { + if !next_file.is_dir { + next_file.load_handle()?; + break; + } + } + Ok(file_contents_resp) + } +} + +#[inline] +pub fn clear_files() { + CLIP_FILES.lock().clear(); +} + +pub fn read_file_contents( + conn_id: i32, + stream_id: i32, + list_index: i32, + dw_flags: i32, + n_position_low: i32, + n_position_high: i32, + cb_requested: i32, +) -> Result { + let fcr = if dw_flags == 0x1 { + FileContentsRequest::Size { + stream_id, + file_idx: list_index as usize, + } + } else if dw_flags == 0x2 { + let offset = (n_position_high as u64) << 32 | n_position_low as u64; + let length = cb_requested as u64; + + FileContentsRequest::Range { + stream_id, + file_idx: list_index as usize, + offset, + length, + } + } else { + return Err(CliprdrError::InvalidRequest { + description: format!("got invalid FileContentsRequest, dw_flats: {dw_flags}"), + }); + }; + + CLIP_FILES.lock().serve_file_contents(conn_id, fcr) +} + +pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> { + let mut files_lock = CLIP_FILES.lock(); + if files_lock.files == files { + return Ok(()); + } + files_lock.sync_files(files)?; + Ok(files_lock.build_file_list_pdu()) +} + +pub fn get_file_list_pdu() -> Vec { + CLIP_FILES.lock().files_pdu.clone() +} diff --git a/libs/clipboard/src/platform/unix/url.rs b/libs/clipboard/src/platform/unix/url.rs deleted file mode 100644 index 126a341cd72..00000000000 --- a/libs/clipboard/src/platform/unix/url.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::path::{Path, PathBuf}; - -use crate::CliprdrError; - -// on x11, path will be encode as -// "/home/rustdesk/pictures/🖼️.png" -> "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png" -// 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: &Path) -> io::Result { - let encoded = - percent_encoding::percent_encode(path.to_str()?.as_bytes(), &ENCODE_SET).to_string(); - format!("file://{}", encoded) -} - -pub(super) fn parse_uri_to_path(encoded_uri: &str) -> Result { - let encoded_path = encoded_uri.trim_start_matches("file://"); - let path_str = percent_encoding::percent_decode_str(encoded_path) - .decode_utf8() - .map_err(|_| CliprdrError::ConversionFailure)?; - let path_str = path_str.to_string(); - - Ok(Path::new(&path_str).to_path_buf()) -} - -// helper parse function -// convert 'text/uri-list' data to a list of valid Paths -// # Note -// - none utf8 data will lead to error -pub(super) fn parse_plain_uri_list(v: Vec) -> Result, CliprdrError> { - let text = String::from_utf8(v).map_err(|_| CliprdrError::ConversionFailure)?; - parse_uri_list(&text) -} - -// helper parse function -// convert 'text/uri-list' data to a list of valid Paths -// # Note -// - none utf8 data will lead to error -pub(super) fn parse_uri_list(text: &str) -> Result, CliprdrError> { - let mut list = Vec::new(); - - for line in text.lines() { - if !line.starts_with("file://") { - continue; - } - let decoded = parse_uri_to_path(line)?; - list.push(decoded) - } - Ok(list) -} - -#[cfg(test)] -mod uri_test { - #[test] - fn test_conversion() { - let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼️.png"); - let uri = super::encode_path_to_uri(&path).unwrap(); - assert_eq!( - uri, - "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png" - ); - let convert_back = super::parse_uri_to_path(&uri).unwrap(); - assert_eq!(path, convert_back); - } - - #[test] - fn parse_list() { - let uri_list = r#"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png -file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png -"#; - let list = super::parse_uri_list(uri_list.into()).unwrap(); - assert!(list.len() == 2); - assert_eq!(list[0], list[1]); - } -} diff --git a/libs/clipboard/src/platform/unix/x11.rs b/libs/clipboard/src/platform/unix/x11.rs deleted file mode 100644 index 606ff671996..00000000000 --- a/libs/clipboard/src/platform/unix/x11.rs +++ /dev/null @@ -1,171 +0,0 @@ -use std::{ - collections::BTreeSet, - path::{Path, PathBuf}, -}; - -use hbb_common::log; -use once_cell::sync::OnceCell; -use parking_lot::Mutex; -use x11_clipboard::Clipboard; -use x11rb::protocol::xproto::Atom; - -use crate::{platform::unix::send_format_list, CliprdrError}; - -use super::{encode_path_to_uri, parse_plain_uri_list, SysClipboard}; - -static X11_CLIPBOARD: OnceCell = OnceCell::new(); - -fn get_clip() -> Result<&'static Clipboard, CliprdrError> { - X11_CLIPBOARD.get_or_try_init(|| Clipboard::new().map_err(|_| CliprdrError::CliprdrInit)) -} - -pub struct X11Clipboard { - ignore_path: PathBuf, - text_uri_list: Atom, - gnome_copied_files: Atom, - nautilus_clipboard: Atom, - - former_file_list: Mutex>, -} - -impl X11Clipboard { - pub fn new(ignore_path: &Path) -> Result { - let clipboard = get_clip()?; - let text_uri_list = clipboard - .setter - .get_atom("text/uri-list") - .map_err(|_| CliprdrError::CliprdrInit)?; - let gnome_copied_files = clipboard - .setter - .get_atom("x-special/gnome-copied-files") - .map_err(|_| CliprdrError::CliprdrInit)?; - let nautilus_clipboard = clipboard - .setter - .get_atom("x-special/nautilus-clipboard") - .map_err(|_| CliprdrError::CliprdrInit)?; - Ok(Self { - ignore_path: ignore_path.to_owned(), - text_uri_list, - gnome_copied_files, - nautilus_clipboard, - former_file_list: Mutex::new(vec![]), - }) - } - - fn load(&self, target: Atom) -> Result, CliprdrError> { - let clip = get_clip()?.setter.atoms.clipboard; - let prop = get_clip()?.setter.atoms.property; - // NOTE: - // # why not use `load_wait` - // load_wait is likely to wait forever, which is not what we want - let res = get_clip()?.load_wait(clip, target, prop); - match res { - Ok(res) => Ok(res), - Err(x11_clipboard::error::Error::UnexpectedType(_)) => Ok(vec![]), - Err(x11_clipboard::error::Error::Timeout) => { - log::debug!("x11 clipboard get content timeout."); - Err(CliprdrError::ClipboardInternalError) - } - Err(e) => { - log::debug!("x11 clipboard get content fail: {:?}", e); - Err(CliprdrError::ClipboardInternalError) - } - } - } - - fn store_batch(&self, batch: Vec<(Atom, Vec)>) -> Result<(), CliprdrError> { - let clip = get_clip()?.setter.atoms.clipboard; - log::debug!("try to store clipboard content"); - get_clip()? - .store_batch(clip, batch) - .map_err(|_| CliprdrError::ClipboardInternalError) - } - - fn wait_file_list(&self) -> Result>, CliprdrError> { - let v = self.load(self.text_uri_list)?; - let p = parse_plain_uri_list(v)?; - Ok(Some(p)) - } -} - -impl SysClipboard for X11Clipboard { - fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { - *self.former_file_list.lock() = paths.to_vec(); - - let uri_list: Vec = { - let mut v = Vec::new(); - for path in paths { - v.push(encode_path_to_uri(path)?); - } - v - }; - let uri_list = uri_list.join("\n"); - let text_uri_list_data = uri_list.as_bytes().to_vec(); - let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat(); - let batch = vec![ - (self.text_uri_list, text_uri_list_data), - (self.gnome_copied_files, gnome_copied_files_data.clone()), - (self.nautilus_clipboard, gnome_copied_files_data), - ]; - self.store_batch(batch) - .map_err(|_| CliprdrError::ClipboardInternalError) - } - - fn start(&self) { - { - // clear cached file list - *self.former_file_list.lock() = vec![]; - } - loop { - let sth = match self.wait_file_list() { - Ok(sth) => sth, - Err(e) => { - log::warn!("failed to get file list from clipboard: {}", e); - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - }; - - let Some(paths) = sth else { - // just sleep - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - }; - - let filtered = paths - .into_iter() - .filter(|pb| !pb.starts_with(&self.ignore_path)) - .collect::>(); - - if filtered.is_empty() { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - - { - let mut former = self.former_file_list.lock(); - - let filtered_st: BTreeSet<_> = filtered.iter().collect(); - let former_st = former.iter().collect::>(); - if filtered_st == former_st { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - - *former = filtered; - } - - if let Err(e) = send_format_list(0) { - log::warn!("failed to send format list: {}", e); - break; - } - - std::thread::sleep(std::time::Duration::from_millis(100)); - } - log::debug!("stop listening file related atoms on clipboard"); - } - - fn get_file_list(&self) -> Vec { - self.former_file_list.lock().clone() - } -} diff --git a/libs/clipboard/src/platform/windows.rs b/libs/clipboard/src/platform/windows.rs index 3b931a4a651..1d8506eade8 100644 --- a/libs/clipboard/src/platform/windows.rs +++ b/libs/clipboard/src/platform/windows.rs @@ -614,6 +614,7 @@ fn ret_to_result(ret: u32) -> Result<(), CliprdrError> { e => Err(CliprdrError::Unknown(e)), } } + pub fn empty_clipboard(context: &mut CliprdrClientContext, conn_id: i32) -> bool { unsafe { TRUE == empty_cliprdr(context, conn_id as u32) } } diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index dc82f1ae2dd..132fbcc13a6 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.7" +version = "1.3.8" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index f27fd6d638d..775108c8f27 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.7 +pkgver=1.3.8 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index d56838d3c3f..add593685f6 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.7 +Version: 1.3.8 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 771c8a12e7f..b30d0f9de65 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.7 +Version: 1.3.8 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index eb4a9a7ad38..502340946c7 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.7 +Version: 1.3.8 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/src/client.rs b/src/client.rs index bdef9c7ed0f..319b3f3da3e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,7 +1,9 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::clipboard_listener; use async_trait::async_trait; use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use clipboard_master::{CallbackResult, ClipboardHandler}; +use clipboard_master::CallbackResult; #[cfg(not(target_os = "linux"))] use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, @@ -15,17 +17,25 @@ use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, ffi::c_void, - io, net::SocketAddr, ops::Deref, str::FromStr, sync::{ - mpsc::{self, RecvTimeoutError, Sender}, + mpsc::{self, RecvTimeoutError}, Arc, Mutex, RwLock, }, }; use uuid::Uuid; +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_builtin_option, use_texture_render}, + ui_session_interface::{InvokeUiSession, Session}, +}; +#[cfg(feature = "unix-file-copy-paste")] +use crate::{clipboard::check_clipboard_files, clipboard_file::unix_file_clip}; pub use file_trait::FileManager; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -62,14 +72,6 @@ use scrap::{ CodecFormat, ImageFormat, ImageRgb, ImageTexture, }; -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_builtin_option, use_texture_render}, - ui_session_interface::{InvokeUiSession, Session}, -}; - #[cfg(not(target_os = "ios"))] use crate::clipboard::CLIPBOARD_INTERVAL; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -128,14 +130,19 @@ pub(crate) struct ClientClipboardContext; pub(crate) struct ClientClipboardContext { pub cfg: SessionPermissionConfig, pub tx: UnboundedSender, + #[cfg(feature = "unix-file-copy-paste")] + pub is_file_supported: bool, } /// Client of the remote desktop. pub struct Client; #[cfg(not(target_os = "ios"))] -struct TextClipboardState { - is_required: bool, +struct ClipboardState { + #[cfg(feature = "flutter")] + is_text_required: bool, + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + is_file_required: bool, running: bool, } @@ -151,7 +158,7 @@ lazy_static::lazy_static! { #[cfg(not(target_os = "ios"))] lazy_static::lazy_static! { - static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); + static ref CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(ClipboardState::new())); } const PUBLIC_SERVER: &str = "public"; @@ -167,6 +174,8 @@ pub fn get_key_state(key: enigo::Key) -> bool { } impl Client { + const CLIENT_CLIPBOARD_NAME: &'static str = "client-clipboard"; + /// Start a new connection. pub async fn start( peer: &str, @@ -657,7 +666,13 @@ impl Client { #[cfg(feature = "flutter")] #[cfg(not(target_os = "ios"))] pub fn set_is_text_clipboard_required(b: bool) { - TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b; + CLIPBOARD_STATE.lock().unwrap().is_text_required = b; + } + + #[inline] + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + pub fn set_is_file_clipboard_required(b: bool) { + CLIPBOARD_STATE.lock().unwrap().is_file_required = b; } #[cfg(not(target_os = "ios"))] @@ -673,68 +688,55 @@ impl Client { if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { return; } - TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + #[cfg(not(target_os = "android"))] + clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME); + CLIPBOARD_STATE.lock().unwrap().running = false; + #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] + clipboard::platform::unix::fuse::uninit_fuse_context(true); } // `try_start_clipboard` is called by all session when connection is established. (When handling peer info). // This function only create one thread with a loop, the loop is shared by all sessions. // After all sessions are end, the loop exists. // - // If clipboard update is detected, the text will be sent to all sessions by `send_text_clipboard_msg`. + // If clipboard update is detected, the text will be sent to all sessions by `send_clipboard_msg`. #[cfg(not(any(target_os = "android", target_os = "ios")))] fn try_start_clipboard( _client_clip_ctx: Option, ) -> Option> { - let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); + let mut clipboard_lock = 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; - } - }; + if let Err(e) = + clipboard_listener::subscribe(Self::CLIENT_CLIPBOARD_NAME.to_owned(), tx_cb_result) + { + log::error!("Failed to subscribe clipboard listener: {}", e); + return None; + } clipboard_lock.running = true; - let (tx_started, rx_started) = unbounded_channel(); - log::info!("Start text clipboard loop"); + log::info!("Start client clipboard loop"); std::thread::spawn(move || { - let mut is_sent = false; + let mut handler = ClientClipboardHandler { + ctx: None, + #[cfg(not(feature = "flutter"))] + client_clip_ctx: _client_clip_ctx, + }; + tx_started.send(()).ok(); loop { - if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { + if !CLIPBOARD_STATE.lock().unwrap().running { break; } - if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required { - std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); - continue; - } - - if !is_sent { - is_sent = true; - tx_started.send(()).ok(); - } - match rx_cb_result.recv_timeout(Duration::from_millis(CLIPBOARD_INTERVAL)) { + Ok(CallbackResult::Next) => { + handler.check_clipboard(); + } Ok(CallbackResult::Stop) => { log::debug!("Clipboard listener stopped"); break; @@ -744,13 +746,14 @@ impl Client { break; } Err(RecvTimeoutError::Timeout) => {} - _ => {} + Err(RecvTimeoutError::Disconnected) => { + log::error!("Clipboard listener disconnected"); + break; + } } } - log::info!("Stop text clipboard loop"); - shutdown.signal(); - h.join().ok(); - TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + log::info!("Stop client clipboard loop"); + CLIPBOARD_STATE.lock().unwrap().running = false; }); Some(rx_started) @@ -758,31 +761,31 @@ impl Client { #[cfg(target_os = "android")] fn try_start_clipboard(_p: Option<()>) -> Option> { - let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); + let mut clipboard_lock = CLIPBOARD_STATE.lock().unwrap(); if clipboard_lock.running { return None; } clipboard_lock.running = true; - log::info!("Start text clipboard loop"); + log::info!("Start client clipboard loop"); std::thread::spawn(move || { loop { - if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { + if !CLIPBOARD_STATE.lock().unwrap().running { break; } - if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required { + if !CLIPBOARD_STATE.lock().unwrap().is_text_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); + crate::flutter::send_clipboard_msg(msg, false); } std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); } - log::info!("Stop text clipboard loop"); - TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + log::info!("Stop client clipboard loop"); + CLIPBOARD_STATE.lock().unwrap().running = false; }); None @@ -790,10 +793,13 @@ impl Client { } #[cfg(not(target_os = "ios"))] -impl TextClipboardState { +impl ClipboardState { fn new() -> Self { Self { - is_required: true, + #[cfg(feature = "flutter")] + is_text_required: true, + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + is_file_required: true, running: false, } } @@ -802,59 +808,102 @@ 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); + fn is_text_required(&self) -> bool { + #[cfg(feature = "flutter")] + { + CLIPBOARD_STATE.lock().unwrap().is_text_required + } + #[cfg(not(feature = "flutter"))] + { + self.client_clip_ctx + .as_ref() + .map(|ctx| ctx.cfg.is_text_clipboard_required()) + .unwrap_or(false) + } } - #[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; + #[cfg(feature = "unix-file-copy-paste")] + fn is_file_required(&self) -> bool { + #[cfg(feature = "flutter")] + { + CLIPBOARD_STATE.lock().unwrap().is_file_required + } + #[cfg(not(feature = "flutter"))] + { + self.client_clip_ctx + .as_ref() + .map(|ctx| ctx.cfg.is_file_clipboard_required()) + .unwrap_or(false) + } + } + + fn check_clipboard(&mut self) { + if CLIPBOARD_STATE.lock().unwrap().running { + #[cfg(feature = "unix-file-copy-paste")] + if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Client, false) { + if !urls.is_empty() { + if self.is_file_required() { + match clipboard::platform::unix::serv_files::sync_files(&urls) { + Ok(()) => { + let msg = crate::clipboard_file::clip_2_msg( + unix_file_clip::get_format_list(), + ); + self.send_msg(msg, true); + } + Err(e) => { + log::error!("Failed to sync clipboard files: {}", e); + } } + 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); + if self.is_text_required() { + self.send_msg(msg, false); + } } } - CallbackResult::Next } - fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { - self.tx_cb_result - .send(CallbackResult::StopWithError(error)) - .ok(); - CallbackResult::Next + #[inline] + #[cfg(feature = "flutter")] + fn send_msg(&self, msg: Message, _is_file: bool) { + crate::flutter::send_clipboard_msg(msg, _is_file); + } + + #[cfg(not(feature = "flutter"))] + fn send_msg(&self, msg: Message, _is_file: bool) { + if let Some(ctx) = &self.client_clip_ctx { + #[cfg(feature = "unix-file-copy-paste")] + if _is_file { + if ctx.is_file_supported { + let _ = ctx.tx.send(Data::Message(msg)); + } + return; + } + + 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)); + } } } @@ -1813,6 +1862,12 @@ impl LoginConfigHandler { self.config.store(&self.id); return None; } + + #[cfg(feature = "unix-file-copy-paste")] + if option.enable_file_transfer.enum_value() == Ok(BoolOption::No) { + crate::clipboard::try_empty_clipboard_files(crate::clipboard::ClipboardSide::Client, 0); + } + if !name.contains("block-input") { self.save_config(config); } @@ -2338,6 +2393,10 @@ impl LoginConfigHandler { }) .ok() } + + pub fn get_id(&self) -> &str { + &self.id + } } /// Media data. @@ -3240,7 +3299,7 @@ pub enum Data { CancelJob(i32), RemovePortForward(i32), AddPortForward((i32, String, i32)), - #[cfg(not(feature = "flutter"))] + #[cfg(all(target_os = "windows", not(feature = "flutter")))] ToggleClipboardFile, NewRDP, SetConfirmOverrideFile((i32, i32, bool, bool, bool)), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index fb7cba3c51c..23d2f409420 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,13 +1,3 @@ -use std::{ - collections::HashMap, - ffi::c_void, - num::NonZeroI64, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, RwLock, - }, -}; - #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::clipboard::{update_clipboard, ClipboardSide}; #[cfg(not(any(target_os = "ios")))] @@ -20,7 +10,9 @@ use crate::{ common::get_default_sound_input, ui_session_interface::{InvokeUiSession, Session}, }; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +#[cfg(feature = "unix-file-copy-paste")] +use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip}; +#[cfg(target_os = "windows")] use clipboard::ContextSend; use crossbeam_queue::ArrayQueue; #[cfg(not(target_os = "ios"))] @@ -44,9 +36,18 @@ use hbb_common::{ }, Stream, }; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] use hbb_common::{tokio::sync::Mutex as TokioMutex, ResultType}; use scrap::CodecFormat; +use std::{ + collections::HashMap, + ffi::c_void, + num::NonZeroI64, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, RwLock, + }, +}; pub struct Remote { handler: Session, @@ -63,7 +64,7 @@ pub struct Remote { last_update_jobs_status: (Instant, HashMap), is_connected: bool, first_frame: bool, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] client_conn_id: i32, // used for file clipboard data_count: Arc, video_format: CodecFormat, @@ -107,7 +108,7 @@ impl Remote { last_update_jobs_status: (Instant::now(), Default::default()), is_connected: false, first_frame: false, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] client_conn_id: 0, data_count: Arc::new(AtomicUsize::new(0)), video_format: CodecFormat::Unknown, @@ -122,7 +123,7 @@ 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"))] + #[cfg(target_os = "windows")] 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() { @@ -175,26 +176,33 @@ impl Remote { } // just build for now - #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] + #[cfg(not(any(target_os = "windows", feature = "unix-file-copy-paste")))] let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] let (_tx_holder, rx) = mpsc::unbounded_channel(); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - let mut rx_clip_client_lock = Arc::new(TokioMutex::new(rx)); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + let mut rx_clip_client_holder = (Arc::new(TokioMutex::new(rx)), None); + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] { let is_conn_not_default = self.handler.is_file_transfer() || self.handler.is_port_forward() || self.handler.is_rdp(); if !is_conn_not_default { - log::debug!("get cliprdr client for conn_id {}", self.client_conn_id); - (self.client_conn_id, rx_clip_client_lock) = + (self.client_conn_id, rx_clip_client_holder.0) = clipboard::get_rx_cliprdr_client(&self.handler.get_id()); + log::debug!("get cliprdr client for conn_id {}", self.client_conn_id); + let client_conn_id = self.client_conn_id; + rx_clip_client_holder.1 = Some(crate::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + clipboard::remove_channel_by_conn_id(client_conn_id); + }), + }); }; } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - let mut rx_clip_client = rx_clip_client_lock.lock().await; + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + let mut rx_clip_client = rx_clip_client_holder.0.lock().await; let mut status_timer = crate::rustdesk_interval(time::interval(Duration::new(1, 0))); @@ -242,8 +250,8 @@ impl Remote { } } _msg = rx_clip_client.recv() => { - #[cfg(any(target_os="windows", target_os="linux", target_os = "macos"))] - self.handle_local_clipboard_msg(&mut peer, _msg).await; + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + self.handle_local_clipboard_msg(&mut peer, _msg).await; } _ = self.timer.tick() => { if last_recv_time.elapsed() >= SEC30 { @@ -323,18 +331,13 @@ impl Remote { Client::try_stop_clipboard(); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] if _set_disconnected_ok { - let conn_id = self.client_conn_id; - log::debug!("try empty cliprdr for conn_id {}", conn_id); - let _ = ContextSend::proc(|context| -> ResultType<()> { - context.empty_clipboard(conn_id)?; - Ok(()) - }); + crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id); } } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] async fn handle_local_clipboard_msg( &self, peer: &mut crate::client::FramedStream, @@ -365,8 +368,12 @@ impl Remote { view_only, stop, is_stopping_allowed, server_file_transfer_enabled, file_transfer_enabled ); if stop { - ContextSend::set_is_stopped(); + #[cfg(target_os = "windows")] + { + ContextSend::set_is_stopped(); + } } else { + #[cfg(target_os = "windows")] 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 @@ -509,7 +516,7 @@ impl Remote { .handle_login_from_ui(os_username, os_password, password, remember, peer) .await; } - #[cfg(not(feature = "flutter"))] + #[cfg(all(target_os = "windows", not(feature = "flutter")))] Data::ToggleClipboardFile => { self.check_clipboard_file_context(); } @@ -1221,7 +1228,7 @@ impl Remote { let peer_platform = pi.platform.clone(); self.set_peer_info(&pi); self.handler.handle_peer_info(pi); - #[cfg(not(feature = "flutter"))] + #[cfg(all(target_os = "windows", not(feature = "flutter")))] self.check_clipboard_file_context(); if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { #[cfg(feature = "flutter")] @@ -1233,6 +1240,10 @@ impl Remote { crate::client::ClientClipboardContext { cfg: self.handler.get_permission_config(), tx: self.sender.clone(), + #[cfg(feature = "unix-file-copy-paste")] + is_file_supported: crate::is_support_file_copy_paste( + &peer_version, + ), }, )); // To make sure current text clipboard data is updated. @@ -1264,6 +1275,9 @@ impl Remote { #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + crate::flutter::update_file_clipboard_required(); + // on connection established client #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1317,9 +1331,9 @@ impl Remote { crate::clipboard::handle_msg_multi_clipboards(_mcb); } } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] Some(message::Union::Cliprdr(clip)) => { - self.handle_cliprdr_msg(clip); + self.handle_cliprdr_msg(clip, peer).await; } Some(message::Union::FileResponse(fr)) => { match fr.union { @@ -1484,6 +1498,8 @@ impl Remote { #[cfg(feature = "flutter")] #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + crate::flutter::update_file_clipboard_required(); self.handler.set_permission("keyboard", p.enabled); } Ok(Permission::Clipboard) => { @@ -1502,7 +1518,16 @@ impl Remote { if !p.enabled && self.handler.is_file_transfer() { return true; } + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + crate::flutter::update_file_clipboard_required(); self.handler.set_permission("file", p.enabled); + #[cfg(feature = "unix-file-copy-paste")] + if !p.enabled { + try_empty_clipboard_files( + ClipboardSide::Client, + self.client_conn_id, + ); + } } Ok(Permission::Restart) => { self.handler.set_permission("restart", p.enabled); @@ -1922,24 +1947,19 @@ impl Remote { true } - #[cfg(not(feature = "flutter"))] + #[cfg(all(target_os = "windows", not(feature = "flutter")))] fn check_clipboard_file_context(&self) { - #[cfg(any( - target_os = "windows", - all( - feature = "unix-file-copy-paste", - any(target_os = "linux", target_os = "macos") - ) - ))] - { - let enabled = *self.handler.server_file_transfer_enabled.read().unwrap() - && self.handler.lc.read().unwrap().enable_file_copy_paste.v; - ContextSend::enable(enabled); - } + let enabled = *self.handler.server_file_transfer_enabled.read().unwrap() + && self.handler.lc.read().unwrap().enable_file_copy_paste.v; + ContextSend::enable(enabled); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - fn handle_cliprdr_msg(&self, clip: hbb_common::message_proto::Cliprdr) { + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + async fn handle_cliprdr_msg( + &self, + clip: hbb_common::message_proto::Cliprdr, + _peer: &mut Stream, + ) { log::debug!("handling cliprdr msg from server peer"); #[cfg(feature = "flutter")] if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { @@ -1956,20 +1976,34 @@ impl Remote { }; let is_stopping_allowed = clip.is_beginning_message(); - let file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_copy_paste.v; + let file_transfer_enabled = self.handler.is_file_clipboard_required(); let stop = is_stopping_allowed && !file_transfer_enabled; log::debug!( "Process clipboard message from server peer, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}", stop, is_stopping_allowed, file_transfer_enabled); if !stop { + #[cfg(target_os = "windows")] if let Err(e) = ContextSend::make_sure_enabled() { log::error!("failed to restart clipboard context: {}", e); }; - let _ = ContextSend::proc(|context| -> ResultType<()> { - context - .server_clip_file(self.client_conn_id, clip) - .map_err(|e| e.into()) - }); + #[cfg(target_os = "windows")] + { + let _ = ContextSend::proc(|context| -> ResultType<()> { + context + .server_clip_file(self.client_conn_id, clip) + .map_err(|e| e.into()) + }); + } + #[cfg(feature = "unix-file-copy-paste")] + if crate::is_support_file_copy_paste_num(self.handler.lc.read().unwrap().version) { + if let Some(msg) = unix_file_clip::serve_clip_messages( + ClipboardSide::Client, + clip, + self.client_conn_id, + ) { + allow_err!(_peer.send(&msg).await); + } + } } } diff --git a/src/clipboard.rs b/src/clipboard.rs index ac3a83f00f7..4ab4bc666df 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,15 +1,14 @@ #[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::{ - sync::{mpsc::Sender, Arc, Mutex}, - thread::JoinHandle, + sync::{Arc, Mutex}, time::Duration, }; pub const CLIPBOARD_NAME: &'static str = "clipboard"; +#[cfg(feature = "unix-file-copy-paste")] +pub const FILE_CLIPBOARD_NAME: &'static str = "file-clipboard"; pub const CLIPBOARD_INTERVAL: u64 = 333; // This format is used to store the flag in the clipboard. @@ -43,115 +42,12 @@ const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ ClipboardFormat::ImageRgba, ClipboardFormat::ImagePng, ClipboardFormat::ImageSvg, + #[cfg(feature = "unix-file-copy-paste")] + ClipboardFormat::FileUrl, ClipboardFormat::Special(CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET), 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(); - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -fn get_clipboard() -> Result<&'static x11_clipboard::Clipboard, String> { - X11_CLIPBOARD - .get_or_try_init(|| x11_clipboard::Clipboard::new()) - .map_err(|e| e.to_string()) -} - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -pub struct ClipboardContext { - string_setter: x11rb::protocol::xproto::Atom, - string_getter: x11rb::protocol::xproto::Atom, - text_uri_list: x11rb::protocol::xproto::Atom, - - clip: x11rb::protocol::xproto::Atom, - prop: x11rb::protocol::xproto::Atom, -} - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -fn parse_plain_uri_list(v: Vec) -> Result { - let text = String::from_utf8(v).map_err(|_| "ConversionFailure".to_owned())?; - let mut list = String::new(); - for line in text.lines() { - if !line.starts_with("file://") { - continue; - } - let decoded = percent_encoding::percent_decode_str(line) - .decode_utf8() - .map_err(|_| "ConversionFailure".to_owned())?; - list = list + "\n" + decoded.trim_start_matches("file://"); - } - list = list.trim().to_owned(); - Ok(list) -} - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -impl ClipboardContext { - pub fn new() -> Result { - let clipboard = get_clipboard()?; - let string_getter = clipboard - .getter - .get_atom("UTF8_STRING") - .map_err(|e| e.to_string())?; - let string_setter = clipboard - .setter - .get_atom("UTF8_STRING") - .map_err(|e| e.to_string())?; - let text_uri_list = clipboard - .getter - .get_atom("text/uri-list") - .map_err(|e| e.to_string())?; - let prop = clipboard.getter.atoms.property; - let clip = clipboard.getter.atoms.clipboard; - Ok(Self { - text_uri_list, - string_setter, - string_getter, - clip, - prop, - }) - } - - pub fn get_text(&mut self) -> Result { - let clip = self.clip; - let prop = self.prop; - - const TIMEOUT: std::time::Duration = std::time::Duration::from_millis(120); - - let text_content = get_clipboard()? - .load(clip, self.string_getter, prop, TIMEOUT) - .map_err(|e| e.to_string())?; - - let file_urls = get_clipboard()?.load(clip, self.text_uri_list, prop, TIMEOUT)?; - - if file_urls.is_err() || file_urls.as_ref().is_empty() { - log::trace!("clipboard get text, no file urls"); - return String::from_utf8(text_content).map_err(|e| e.to_string()); - } - - let file_urls = parse_plain_uri_list(file_urls)?; - - let text_content = String::from_utf8(text_content).map_err(|e| e.to_string())?; - - if text_content.trim() == file_urls.trim() { - log::trace!("clipboard got text but polluted"); - return Err(String::from("polluted text")); - } - - Ok(text_content) - } - - pub fn set_text(&mut self, content: String) -> Result<(), String> { - let clip = self.clip; - - let value = content.clone().into_bytes(); - get_clipboard()? - .store(clip, self.string_setter, value) - .map_err(|e| e.to_string())?; - Ok(()) - } -} - #[cfg(not(target_os = "android"))] pub fn check_clipboard( ctx: &mut Option, @@ -179,6 +75,73 @@ pub fn check_clipboard( None } +#[cfg(feature = "unix-file-copy-paste")] +pub fn check_clipboard_files( + ctx: &mut Option, + side: ClipboardSide, + force: bool, +) -> Option> { + if ctx.is_none() { + *ctx = ClipboardContext::new().ok(); + } + let ctx2 = ctx.as_mut()?; + match ctx2.get_files(side, force) { + Ok(Some(urls)) => { + if !urls.is_empty() { + return Some(urls); + } + } + Err(e) => { + log::error!("Failed to get clipboard file urls. {}", e); + } + _ => {} + } + None +} + +#[cfg(feature = "unix-file-copy-paste")] +pub fn update_clipboard_files(files: Vec, side: ClipboardSide) { + if !files.is_empty() { + std::thread::spawn(move || { + do_update_clipboard_(vec![ClipboardData::FileUrl(files)], side); + }); + } +} + +#[cfg(feature = "unix-file-copy-paste")] +pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) { + #[cfg(target_os = "linux")] + std::thread::spawn(move || { + let mut ctx = CLIPBOARD_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; + } + } + } + if let Some(mut ctx) = ctx.as_mut() { + use clipboard::platform::unix; + if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) { + ctx.try_empty_clipboard_files(_side); + } + } + }); +} + +#[cfg(target_os = "windows")] +pub fn try_empty_clipboard_files(side: ClipboardSide, conn_id: i32) { + log::debug!("try to empty {} cliprdr for conn_id {}", side, conn_id); + let _ = clipboard::ContextSend::proc(|context| -> ResultType<()> { + context.empty_clipboard(conn_id)?; + Ok(()) + }); +} + #[cfg(target_os = "windows")] pub fn check_clipboard_cm() -> ResultType { let mut ctx = CLIPBOARD_CTX.lock().unwrap(); @@ -203,10 +166,15 @@ 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); + let to_update_data = proto::from_multi_clipbards(multi_clipboards); if to_update_data.is_empty() { return; } + do_update_clipboard_(to_update_data, side); +} + +#[cfg(not(target_os = "android"))] +fn do_update_clipboard_(mut to_update_data: Vec, side: ClipboardSide) { let mut ctx = CLIPBOARD_CTX.lock().unwrap(); if ctx.is_none() { match ClipboardContext::new() { @@ -240,13 +208,11 @@ pub fn update_clipboard(multi_clipboards: Vec, side: ClipboardSide) { } #[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 { pub fn new() -> ResultType { @@ -293,7 +259,7 @@ impl ClipboardContext { // 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) { + match self.inner.get_formats(formats) { Ok(data) => { return Ok(data .into_iter() @@ -316,8 +282,26 @@ impl ClipboardContext { } pub fn get(&mut self, side: ClipboardSide, force: bool) -> ResultType> { + let data = self.get_formats_filter(SUPPORTED_FORMATS, side, force)?; + // We have a seperate service named `file-clipboard` to handle file copy-paste. + // We need to read the file urls because file copy may set the other clipboard formats such as text. + #[cfg(feature = "unix-file-copy-paste")] + { + if data.iter().any(|c| matches!(c, ClipboardData::FileUrl(_))) { + return Ok(vec![]); + } + } + Ok(data) + } + + fn get_formats_filter( + &mut self, + formats: &[ClipboardFormat], + side: ClipboardSide, + force: bool, + ) -> ResultType> { let _lock = ARBOARD_MTX.lock().unwrap(); - let data = self.get_formats(SUPPORTED_FORMATS)?; + let data = self.get_formats(formats)?; if data.is_empty() { return Ok(data); } @@ -334,16 +318,98 @@ impl ClipboardContext { .into_iter() .filter(|c| match c { ClipboardData::Special((s, _)) => s != RUSTDESK_CLIPBOARD_OWNER_FORMAT, + // Skip synchronizing empty text to the remote clipboard + ClipboardData::Text(text) => !text.is_empty(), _ => true, }) .collect()) } + #[cfg(feature = "unix-file-copy-paste")] + pub fn get_files( + &mut self, + side: ClipboardSide, + force: bool, + ) -> ResultType>> { + let data = self.get_formats_filter( + &[ + ClipboardFormat::FileUrl, + ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT), + ], + side, + force, + )?; + Ok(data.into_iter().find_map(|c| match c { + ClipboardData::FileUrl(urls) => Some(urls), + _ => None, + })) + } + fn set(&mut self, data: &[ClipboardData]) -> ResultType<()> { let _lock = ARBOARD_MTX.lock().unwrap(); self.inner.set_formats(data)?; Ok(()) } + + #[cfg(feature = "unix-file-copy-paste")] + fn try_empty_clipboard_files(&mut self, side: ClipboardSide) { + let _lock = ARBOARD_MTX.lock().unwrap(); + if let Ok(data) = self.get_formats(&[ClipboardFormat::FileUrl]) { + #[cfg(target_os = "linux")] + let exclude_path = + clipboard::platform::unix::fuse::get_exclude_paths(side == ClipboardSide::Client); + #[cfg(target_os = "macos")] + let exclude_path: Arc = Default::default(); + let urls = data + .into_iter() + .filter_map(|c| match c { + ClipboardData::FileUrl(urls) => Some( + urls.into_iter() + .filter(|s| s.starts_with(&*exclude_path)) + .collect::>(), + ), + _ => None, + }) + .flatten() + .collect::>(); + if !urls.is_empty() { + // FIXME: + // The host-side clear file clipboard `let _ = self.inner.clear();`, + // does not work on KDE Plasma for the installed version. + + // Don't use `hbb_common::platform::linux::is_kde()` here. + // It's not correct in the server process. + #[cfg(target_os = "linux")] + let is_kde_x11 = { + let is_kde = std::process::Command::new("sh") + .arg("-c") + .arg("ps -e | grep -E kded[0-9]+ | grep -v grep") + .stdout(std::process::Stdio::piped()) + .output() + .map(|o| !o.stdout.is_empty()) + .unwrap_or(false); + is_kde && crate::platform::linux::is_x11() + }; + #[cfg(target_os = "macos")] + let is_kde_x11 = false; + let clear_holder_text = if is_kde_x11 { + "RustDesk placeholder to clear the file clipbard" + } else { + "" + } + .to_string(); + self.inner + .set_formats(&[ + ClipboardData::Text(clear_holder_text), + ClipboardData::Special(( + RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(), + side.get_owner_data(), + )), + ]) + .ok(); + } + } + } } pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool { @@ -427,36 +493,6 @@ 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)>, -) -> 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 { #[cfg(not(target_os = "android"))] @@ -671,3 +707,140 @@ pub fn get_clipboards_msg(client: bool) -> Option { msg.set_multi_clipboards(clipboards); Some(msg) } + +// We need this mod to notify multiple subscribers when the clipboard changes. +// Because only one clipboard master(listener) can tigger the clipboard change event multiple listeners are created on Linux(x11). +// https://github.com/rustdesk-org/clipboard-master/blob/4fb62e5b62fb6350d82b571ec7ba94b3cd466695/src/master/x11.rs#L226 +#[cfg(not(target_os = "android"))] +pub mod clipboard_listener { + use clipboard_master::{CallbackResult, ClipboardHandler, Master, Shutdown}; + use hbb_common::{bail, log, ResultType}; + use std::{ + collections::HashMap, + io, + sync::mpsc::{channel, Sender}, + sync::{Arc, Mutex}, + thread::JoinHandle, + }; + + lazy_static::lazy_static! { + pub static ref CLIPBOARD_LISTENER: Arc> = Default::default(); + } + + struct Handler { + subscribers: Arc>>>, + } + + impl ClipboardHandler for Handler { + fn on_clipboard_change(&mut self) -> CallbackResult { + let sub_lock = self.subscribers.lock().unwrap(); + for tx in sub_lock.values() { + tx.send(CallbackResult::Next).ok(); + } + CallbackResult::Next + } + + fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { + let msg = format!("Clipboard listener error: {}", error); + let sub_lock = self.subscribers.lock().unwrap(); + for tx in sub_lock.values() { + tx.send(CallbackResult::StopWithError(io::Error::new( + io::ErrorKind::Other, + msg.clone(), + ))) + .ok(); + } + CallbackResult::Next + } + } + + #[derive(Default)] + pub struct ClipboardListener { + subscribers: Arc>>>, + handle: Option<(Shutdown, JoinHandle<()>)>, + } + + pub fn subscribe(name: String, tx: Sender) -> ResultType<()> { + log::info!("Subscribe clipboard listener: {}", &name); + let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap(); + listener_lock + .subscribers + .lock() + .unwrap() + .insert(name.clone(), tx); + + if listener_lock.handle.is_none() { + log::info!("Start clipboard listener thread"); + let handler = Handler { + subscribers: listener_lock.subscribers.clone(), + }; + let (tx_start_res, rx_start_res) = channel(); + let h = 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); + } + }; + listener_lock.handle = Some((shutdown, h)); + log::info!("Clipboard listener thread started"); + } + + log::info!("Clipboard listener subscribed: {}", name); + Ok(()) + } + + pub fn unsubscribe(name: &str) { + log::info!("Unsubscribe clipboard listener: {}", name); + let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap(); + let is_empty = { + let mut sub_lock = listener_lock.subscribers.lock().unwrap(); + if let Some(tx) = sub_lock.remove(name) { + tx.send(CallbackResult::Stop).ok(); + } + sub_lock.is_empty() + }; + if is_empty { + if let Some((shutdown, h)) = listener_lock.handle.take() { + log::info!("Stop clipboard listener thread"); + shutdown.signal(); + h.join().ok(); + log::info!("Clipboard listener thread stopped"); + } + } + log::info!("Clipboard listener unsubscribed: {}", name); + } + + 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 + } +} diff --git a/src/clipboard_file.rs b/src/clipboard_file.rs index 4548cdbea1a..d7c72f981c5 100644 --- a/src/clipboard_file.rs +++ b/src/clipboard_file.rs @@ -189,3 +189,206 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option { _ => None, } } + +#[cfg(feature = "unix-file-copy-paste")] +pub mod unix_file_clip { + use crate::clipboard::try_empty_clipboard_files; + + use super::{ + super::clipboard::{update_clipboard_files, ClipboardSide}, + *, + }; + #[cfg(target_os = "linux")] + use clipboard::platform::unix::fuse; + use clipboard::platform::unix::{ + get_local_format, serv_files, FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME, + FILEDESCRIPTORW_FORMAT_NAME, FILEDESCRIPTOR_FORMAT_ID, + }; + use hbb_common::log; + use std::sync::{Arc, Mutex}; + + lazy_static::lazy_static! { + static ref CLIPBOARD_CTX: Arc>> = Arc::new(Mutex::new(None)); + } + + pub fn get_format_list() -> ClipboardFile { + let fd_format_name = get_local_format(FILEDESCRIPTOR_FORMAT_ID) + .unwrap_or(FILEDESCRIPTORW_FORMAT_NAME.to_string()); + let fc_format_name = get_local_format(FILECONTENTS_FORMAT_ID) + .unwrap_or(FILECONTENTS_FORMAT_NAME.to_string()); + ClipboardFile::FormatList { + format_list: vec![ + (FILEDESCRIPTOR_FORMAT_ID, fd_format_name), + (FILECONTENTS_FORMAT_ID, fc_format_name), + ], + } + } + + #[inline] + fn msg_resp_format_data_failure() -> Message { + clip_2_msg(ClipboardFile::FormatDataResponse { + msg_flags: 0x2, + format_data: vec![], + }) + } + + #[inline] + fn resp_file_contents_fail(stream_id: i32) -> Message { + clip_2_msg(ClipboardFile::FileContentsResponse { + msg_flags: 0x2, + stream_id, + requested_data: vec![], + }) + } + + pub fn serve_clip_messages( + side: ClipboardSide, + clip: ClipboardFile, + conn_id: i32, + ) -> Option { + log::debug!("got clipfile from client peer"); + match clip { + ClipboardFile::MonitorReady => { + log::debug!("client is ready for clipboard"); + } + ClipboardFile::FormatList { format_list } => { + if !format_list + .iter() + .find(|(_, name)| name == FILECONTENTS_FORMAT_NAME) + .map(|(id, _)| *id) + .is_some() + { + log::error!("no file contents format found"); + return None; + }; + let Some(file_descriptor_id) = format_list + .iter() + .find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME) + .map(|(id, _)| *id) + else { + log::error!("no file descriptor format found"); + return None; + }; + // sync file system from peer + let data = ClipboardFile::FormatDataRequest { + requested_format_id: file_descriptor_id, + }; + return Some(clip_2_msg(data)); + } + ClipboardFile::FormatListResponse { + msg_flags: _msg_flags, + } => {} + ClipboardFile::FormatDataRequest { + requested_format_id: _requested_format_id, + } => { + log::debug!("requested format id: {}", _requested_format_id); + let format_data = serv_files::get_file_list_pdu(); + if !format_data.is_empty() { + return Some(clip_2_msg(ClipboardFile::FormatDataResponse { + msg_flags: 1, + format_data, + })); + } + // empty file list, send failure message + return Some(msg_resp_format_data_failure()); + } + #[cfg(target_os = "linux")] + ClipboardFile::FormatDataResponse { + msg_flags, + format_data, + } => { + log::debug!("format data response: msg_flags: {}", msg_flags); + + if msg_flags != 0x1 { + // return failure message? + } + + log::debug!("parsing file descriptors"); + if fuse::init_fuse_context(true).is_ok() { + match fuse::format_data_response_to_urls( + side == ClipboardSide::Client, + format_data, + conn_id, + ) { + Ok(files) => { + update_clipboard_files(files, side); + } + Err(e) => { + log::error!("failed to parse file descriptors: {:?}", e); + } + } + } else { + // send error message to server + } + } + ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + .. + } => { + log::debug!("file contents request: stream_id: {}, list_index: {}, dw_flags: {}, n_position_low: {}, n_position_high: {}, cb_requested: {}", stream_id, list_index, dw_flags, n_position_low, n_position_high, cb_requested); + match serv_files::read_file_contents( + conn_id, + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + ) { + Ok(data) => { + return Some(clip_2_msg(data)); + } + Err(e) => { + log::error!("failed to read file contents: {:?}", e); + return Some(resp_file_contents_fail(stream_id)); + } + } + } + #[cfg(target_os = "linux")] + ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + .. + } => { + log::debug!( + "file contents response: msg_flags: {}, stream_id: {}", + msg_flags, + stream_id, + ); + if fuse::init_fuse_context(true).is_ok() { + hbb_common::allow_err!(fuse::handle_file_content_response( + side == ClipboardSide::Client, + clip + )); + } else { + // send error message to server + } + } + ClipboardFile::NotifyCallback { + r#type, + title, + text, + } => { + // unreachable, but still log it + log::debug!( + "notify callback: type: {}, title: {}, text: {}", + r#type, + title, + text + ); + } + ClipboardFile::TryEmpty => { + try_empty_clipboard_files(side, conn_id); + } + _ => { + log::error!("unsupported clipboard file type"); + } + } + None + } +} diff --git a/src/common.rs b/src/common.rs index 14db9b5d895..bcd7d3647db 100644 --- a/src/common.rs +++ b/src/common.rs @@ -89,7 +89,7 @@ lazy_static::lazy_static! { pub struct SimpleCallOnReturn { pub b: bool, - pub f: Box, + pub f: Box, } impl Drop for SimpleCallOnReturn { @@ -127,6 +127,18 @@ pub fn is_support_multi_ui_session_num(ver: i64) -> bool { ver >= hbb_common::get_version_number(MIN_VER_MULTI_UI_SESSION) } +#[inline] +#[cfg(feature = "unix-file-copy-paste")] +pub fn is_support_file_copy_paste(ver: &str) -> bool { + is_support_file_copy_paste_num(hbb_common::get_version_number(ver)) +} + +#[inline] +#[cfg(feature = "unix-file-copy-paste")] +pub fn is_support_file_copy_paste_num(ver: i64) -> bool { + ver >= hbb_common::get_version_number("1.3.8") +} + // is server process, with "--server" args #[inline] pub fn is_server() -> bool { @@ -751,7 +763,6 @@ pub fn get_sysinfo() -> serde_json::Value { os = format!("{os} - {}", system.os_version().unwrap_or_default()); } let hostname = hostname(); // sys.hostname() return localhost on android in my test - use serde_json::json; #[cfg(any(target_os = "android", target_os = "ios"))] let out; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1057,7 +1068,6 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin } 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)); fd_json.insert("path".into(), json!(path)); diff --git a/src/flutter.rs b/src/flutter.rs index 255a00e0fd5..2bc8066edad 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1305,9 +1305,26 @@ pub fn update_text_clipboard_required() { Client::set_is_text_clipboard_required(is_required); } +#[cfg(feature = "unix-file-copy-paste")] +pub fn update_file_clipboard_required() { + let is_required = sessions::get_sessions() + .iter() + .any(|s| s.is_file_clipboard_required()); + Client::set_is_file_clipboard_required(is_required); +} + #[cfg(not(target_os = "ios"))] -pub fn send_text_clipboard_msg(msg: Message) { +pub fn send_clipboard_msg(msg: Message, _is_file: bool) { for s in sessions::get_sessions() { + #[cfg(feature = "unix-file-copy-paste")] + if _is_file { + if crate::is_support_file_copy_paste_num(s.lc.read().unwrap().version) + && s.is_file_clipboard_required() + { + s.send(Data::Message(msg.clone())); + } + continue; + } if s.is_text_clipboard_required() { // Check if the client supports multi clipboards if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 9e23b7b0267..5c1925dfd9a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -275,6 +275,12 @@ pub fn session_toggle_option(session_id: SessionID, value: String) { if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" { crate::flutter::update_text_clipboard_required(); } + #[cfg(feature = "unix-file-copy-paste")] + if sessions::get_session_by_session_id(&session_id).is_some() + && value == config::keys::OPTION_ENABLE_FILE_COPY_PASTE + { + crate::flutter::update_file_clipboard_required(); + } } pub fn session_toggle_privacy_mode(session_id: SessionID, impl_key: String, on: bool) { @@ -1948,13 +1954,7 @@ pub fn main_hide_dock() -> SyncReturn { } pub fn main_has_file_clipboard() -> SyncReturn { - let ret = cfg!(any( - target_os = "windows", - all( - feature = "unix-file-copy-paste", - any(target_os = "linux", target_os = "macos") - ) - )); + let ret = cfg!(any(target_os = "windows", feature = "unix-file-copy-paste",)); SyncReturn(ret) } diff --git a/src/ipc.rs b/src/ipc.rs index f1deb5ba8e5..5f533cd94ab 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -25,9 +25,7 @@ use hbb_common::{ config::{self, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, password_security as password, - sodiumoxide::base64, - timeout, + log, password_security as password, timeout, tokio::{ self, io::{AsyncRead, AsyncWrite}, @@ -230,7 +228,7 @@ pub enum Data { FS(FS), Test, SyncConfig(Option>), - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(target_os = "windows")] ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), #[cfg(target_os = "windows")] diff --git a/src/server.rs b/src/server.rs index ba1682f3d0f..117b700c17b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -106,7 +106,13 @@ pub fn new() -> ServerPtr { #[cfg(not(target_os = "ios"))] { server.add_service(Box::new(display_service::new())); - server.add_service(Box::new(clipboard_service::new())); + server.add_service(Box::new(clipboard_service::new( + clipboard_service::NAME.to_owned(), + ))); + #[cfg(feature = "unix-file-copy-paste")] + server.add_service(Box::new(clipboard_service::new( + clipboard_service::FILE_NAME.to_owned(), + ))); } #[cfg(not(any(target_os = "android", target_os = "ios")))] { diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 8ae48250055..a3cb65174cc 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -1,18 +1,27 @@ use super::*; #[cfg(not(target_os = "android"))] +use crate::clipboard::clipboard_listener; +#[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(feature = "unix-file-copy-paste")] +pub use crate::{ + clipboard::{check_clipboard_files, FILE_CLIPBOARD_NAME as FILE_NAME}, + clipboard_file::unix_file_clip, +}; +#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] +use clipboard::platform::unix::fuse::{init_fuse_context, uninit_fuse_context}; #[cfg(not(target_os = "android"))] -use clipboard_master::{CallbackResult, ClipboardHandler}; +use clipboard_master::CallbackResult; #[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}, + sync::mpsc::{channel, RecvTimeoutError}, time::Duration, }; #[cfg(windows)] @@ -23,9 +32,7 @@ static CLIPBOARD_SERVICE_OK: AtomicBool = AtomicBool::new(false); #[cfg(not(target_os = "android"))] struct Handler { - sp: EmptyExtraFieldService, ctx: Option, - tx_cb_result: Sender, #[cfg(target_os = "windows")] stream: Option>, #[cfg(target_os = "windows")] @@ -37,39 +44,51 @@ 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); +pub fn new(name: String) -> GenericService { + let svc = EmptyExtraFieldService::new(name, false); GenericService::run(&svc.clone(), run); svc.sp } #[cfg(not(target_os = "android"))] fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] + let _fuse_call_on_ret = { + if sp.name() == FILE_NAME { + Some(init_fuse_context(false).map(|_| crate::SimpleCallOnReturn { + b: true, + f: Box::new(|| { + uninit_fuse_context(false); + }), + })) + } else { + None + } + }; + let (tx_cb_result, rx_cb_result) = channel(); - let handler = Handler { - sp: sp.clone(), - ctx: Some(ClipboardContext::new()?), - tx_cb_result, + let ctx = Some(ClipboardContext::new().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?); + clipboard_listener::subscribe(sp.name(), tx_cb_result)?; + let mut handler = Handler { + ctx, #[cfg(target_os = "windows")] stream: None, #[cfg(target_os = "windows")] rt: None, }; - 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::Next) => { + #[cfg(feature = "unix-file-copy-paste")] + if sp.name() == FILE_NAME { + handler.check_clipboard_file(); + continue; + } + if let Some(msg) = handler.get_clipboard_msg() { + sp.send(msg); + } + } Ok(CallbackResult::Stop) => { log::debug!("Clipboard listener stopped"); break; @@ -78,36 +97,40 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { bail!("Clipboard listener stopped with error: {}", err); } Err(RecvTimeoutError::Timeout) => {} - _ => {} + Err(RecvTimeoutError::Disconnected) => { + log::error!("Clipboard listener disconnected"); + break; + } } } - shutdown.signal(); - h.join().ok(); + + clipboard_listener::unsubscribe(&sp.name()); Ok(()) } #[cfg(not(target_os = "android"))] -impl ClipboardHandler for Handler { - fn on_clipboard_change(&mut self) -> CallbackResult { - if self.sp.ok() { - if let Some(msg) = self.get_clipboard_msg() { - self.sp.send(msg); +impl Handler { + #[cfg(feature = "unix-file-copy-paste")] + fn check_clipboard_file(&mut self) { + if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Host, false) { + if !urls.is_empty() { + match clipboard::platform::unix::serv_files::sync_files(&urls) { + Ok(()) => { + // Use `send_data()` here to reuse `handle_file_clip()` in `connection.rs`. + hbb_common::allow_err!(clipboard::send_data( + 0, + unix_file_clip::get_format_list() + )); + } + Err(e) => { + log::error!("Failed to sync clipboard files: {}", e); + } + } } } - CallbackResult::Next } - fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { - self.tx_cb_result - .send(CallbackResult::StopWithError(error)) - .ok(); - CallbackResult::Next - } -} - -#[cfg(not(target_os = "android"))] -impl Handler { fn get_clipboard_msg(&mut self) -> Option { #[cfg(target_os = "windows")] if crate::common::is_server() && crate::platform::is_root() { @@ -144,6 +167,7 @@ impl Handler { } } } + check_clipboard(&mut self.ctx, ClipboardSide::Host, false) } diff --git a/src/server/connection.rs b/src/server/connection.rs index 326d128777f..4ac552a42ec 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,4 +1,6 @@ use super::{input_service::*, *}; +#[cfg(feature = "unix-file-copy-paste")] +use crate::clipboard::try_empty_clipboard_files; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::clipboard::{update_clipboard, ClipboardSide}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] @@ -6,8 +8,6 @@ use crate::clipboard_file::*; #[cfg(target_os = "android")] use crate::keyboard::client::map_key_to_control_key; #[cfg(target_os = "linux")] -use crate::platform::linux::is_x11; -#[cfg(target_os = "linux")] use crate::platform::linux_desktop_manager; #[cfg(any(target_os = "windows", target_os = "linux"))] use crate::platform::WallPaperRemover; @@ -441,6 +441,28 @@ impl Connection { std::thread::spawn(move || Self::handle_input(_rx_input, tx_cloned)); let mut second_timer = crate::rustdesk_interval(time::interval(Duration::from_secs(1))); + #[cfg(feature = "unix-file-copy-paste")] + let rx_clip_holder; + let mut rx_clip; + let _tx_clip: mpsc::UnboundedSender; + #[cfg(feature = "unix-file-copy-paste")] + { + rx_clip_holder = ( + clipboard::get_rx_cliprdr_server(id), + crate::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + clipboard::remove_channel_by_conn_id(id); + }), + }, + ); + rx_clip = rx_clip_holder.0.lock().await; + } + #[cfg(not(feature = "unix-file-copy-paste"))] + { + (_tx_clip, rx_clip) = mpsc::unbounded_channel::(); + } + loop { tokio::select! { // biased; // video has higher priority // causing test_delay_timer failed while transferring big file @@ -488,6 +510,12 @@ impl Connection { s.write().unwrap().subscribe( super::clipboard_service::NAME, conn.inner.clone(), conn.can_sub_clipboard_service()); + #[cfg(feature = "unix-file-copy-paste")] + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + conn.inner.clone(), + conn.can_sub_file_clipboard_service(), + ); s.write().unwrap().subscribe( NAME_CURSOR, conn.inner.clone(), enabled || conn.show_remote_cursor); @@ -513,6 +541,18 @@ impl Connection { } else if &name == "file" { conn.file = enabled; conn.send_permission(Permission::File, enabled).await; + #[cfg(feature = "unix-file-copy-paste")] + if !enabled { + conn.try_empty_file_clipboard(); + } + #[cfg(feature = "unix-file-copy-paste")] + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + conn.inner.clone(), + conn.can_sub_file_clipboard_service(), + ); + } } else if &name == "restart" { conn.restart = enabled; conn.send_permission(Permission::Restart, enabled).await; @@ -527,7 +567,7 @@ impl Connection { ipc::Data::RawMessage(bytes) => { allow_err!(conn.stream.send_raw(bytes).await); } - #[cfg(any(target_os="windows", target_os="linux", target_os = "macos"))] + #[cfg(target_os = "windows")] ipc::Data::ClipboardFile(clip) => { allow_err!(conn.stream.send(&clip_2_msg(clip)).await); } @@ -740,9 +780,26 @@ impl Connection { } } } + clip_file = rx_clip.recv() => match clip_file { + Some(_clip) => { + #[cfg(feature = "unix-file-copy-paste")] + if crate::is_support_file_copy_paste(&conn.lr.version) + { + conn.handle_file_clip(_clip).await; + } + } + None => { + // + } + }, } } + #[cfg(feature = "unix-file-copy-paste")] + { + conn.try_empty_file_clipboard(); + } + if let Some(video_privacy_conn_id) = privacy_mode::get_privacy_mode_conn_id() { if video_privacy_conn_id == id { let _ = Self::turn_off_privacy_to_msg(id); @@ -1202,15 +1259,20 @@ impl Connection { ); } - #[cfg(any( - target_os = "windows", - all( - any(target_os = "linux", target_os = "macos"), - feature = "unix-file-copy-paste" - ) - ))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] { - platform_additions.insert("has_file_clipboard".into(), json!(true)); + let is_both_windows = cfg!(target_os = "windows") + && self.lr.my_platform == whoami::Platform::Windows.to_string(); + #[cfg(feature = "unix-file-copy-paste")] + let is_unix_and_peer_supported = crate::is_support_file_copy_paste(&self.lr.version); + #[cfg(not(feature = "unix-file-copy-paste"))] + let is_unix_and_peer_supported = false; + // to-do: add file clipboard support for macos + let is_both_macos = cfg!(target_os = "macos") + && self.lr.my_platform == whoami::Platform::MacOS.to_string(); + let has_file_clipboard = + is_both_windows || (is_unix_and_peer_supported && !is_both_macos); + platform_additions.insert("has_file_clipboard".into(), json!(has_file_clipboard)); } #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] @@ -1375,6 +1437,10 @@ impl Connection { if !self.can_sub_clipboard_service() { noperms.push(super::clipboard_service::NAME); } + #[cfg(feature = "unix-file-copy-paste")] + if !self.can_sub_file_clipboard_service() { + noperms.push(super::clipboard_service::FILE_NAME); + } if !self.audio_enabled() { noperms.push(super::audio_service::NAME); } @@ -1455,11 +1521,18 @@ impl Connection { self.audio && !self.disable_audio } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] fn file_transfer_enabled(&self) -> bool { self.file && self.enable_file_transfer } + #[cfg(feature = "unix-file-copy-paste")] + fn can_sub_file_clipboard_service(&self) -> bool { + self.clipboard_enabled() + && self.file_transfer_enabled() + && crate::get_builtin_option(keys::OPTION_ONE_WAY_FILE_TRANSFER) != "Y" + } + fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) { self.send_to_cm(ipc::Data::Login { id: self.inner.id(), @@ -2113,12 +2186,23 @@ impl Connection { #[cfg(target_os = "android")] crate::clipboard::handle_msg_multi_clipboards(_mcb); } - Some(message::Union::Cliprdr(_clip)) => - { - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - if let Some(clip) = msg_2_clip(_clip) { - log::debug!("got clipfile from client peer"); - self.send_to_cm(ipc::Data::ClipboardFile(clip)) + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + Some(message::Union::Cliprdr(clip)) => { + if let Some(clip) = msg_2_clip(clip) { + #[cfg(target_os = "windows")] + { + self.send_to_cm(ipc::Data::ClipboardFile(clip)); + } + #[cfg(feature = "unix-file-copy-paste")] + if crate::is_support_file_copy_paste(&self.lr.version) { + if let Some(msg) = unix_file_clip::serve_clip_messages( + ClipboardSide::Host, + clip, + self.inner.id(), + ) { + self.send(msg).await; + } + } } } Some(message::Union::FileAction(fa)) => { @@ -2911,13 +2995,26 @@ impl Connection { } } } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] if let Ok(q) = o.enable_file_transfer.enum_value() { if q != BoolOption::NotSet { self.enable_file_transfer = q == BoolOption::Yes; + #[cfg(target_os = "windows")] self.send_to_cm(ipc::Data::ClipboardFileEnabled( self.file_transfer_enabled(), )); + #[cfg(feature = "unix-file-copy-paste")] + if !self.enable_file_transfer { + self.try_empty_file_clipboard(); + } + #[cfg(feature = "unix-file-copy-paste")] + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + self.inner.clone(), + self.can_sub_file_clipboard_service(), + ); + } } } if let Ok(q) = o.disable_clipboard.enum_value() { @@ -2941,6 +3038,12 @@ impl Connection { self.inner.clone(), self.can_sub_clipboard_service(), ); + #[cfg(feature = "unix-file-copy-paste")] + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + self.inner.clone(), + self.can_sub_file_clipboard_service(), + ); s.write().unwrap().subscribe( NAME_CURSOR, self.inner.clone(), @@ -3330,6 +3433,41 @@ impl Connection { } false } + + #[cfg(feature = "unix-file-copy-paste")] + async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) { + let is_stopping_allowed = clip.is_stopping_allowed(); + let is_keyboard_enabled = self.peer_keyboard_enabled(); + let file_transfer_enabled = self.file_transfer_enabled(); + let stop = is_stopping_allowed && !file_transfer_enabled; + log::debug!( + "Process clipboard message from clip, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}", + stop, is_stopping_allowed, file_transfer_enabled); + if !stop { + use hbb_common::config::keys::OPTION_ONE_WAY_FILE_TRANSFER; + // Note: Code will not reach here if `crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) == "Y"` is true. + // Because `file-clipboard` service will not be subscribed. + // But we still check it here to keep the same logic to windows version in `ui_cm_interface.rs`. + 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 + } else { + // Maybe we should end the connection, because copy&paste files causes everything to wait. + allow_err!( + self.stream + .send(&crate::clipboard_file::clip_2_msg(clip)) + .await + ); + } + } + } + + #[inline] + #[cfg(feature = "unix-file-copy-paste")] + fn try_empty_file_clipboard(&mut self) { + try_empty_clipboard_files(ClipboardSide::Host, self.inner.id()); + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 5560cb95ed3..6c64e0d9fdb 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -21,12 +21,6 @@ pub fn init() { } fn map_err_scrap(err: String) -> io::Error { - // to-do: Remove this the following log - log::error!( - "REMOVE ME ===================================== wayland scrap error {}", - &err - ); - // to-do: Handle error better, do not restart server if err.starts_with("Did not receive a reply") { log::error!("Fatal pipewire error, {}", &err); diff --git a/src/ui/header.tis b/src/ui/header.tis index 4b634cf54c5..56f3af21e6b 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -174,6 +174,13 @@ class Header: Reactor.Component { } } + var is_file_copy_paste_supported = false; + if (handler.version_cmp(pi.version, '1.2.4') < 0) { + is_file_copy_paste_supported = is_win && pi.platform == "Windows"; + } else { + is_file_copy_paste_supported = handler.has_file_clipboard() && pi.platform_additions.has_file_clipboard; + } + return
  • {translate('Adjust Window')}
  • @@ -201,7 +208,7 @@ class Header: Reactor.Component { {
  • {svg_checkmark}{translate('Follow remote window focus')}
  • }
  • {svg_checkmark}{translate('Show quality monitor')}
  • {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} - {(is_win && pi.platform == "Windows") && file_enabled ?
  • {svg_checkmark}{translate('Enable file copy and paste')}
  • : ""} + {is_file_copy_paste_supported && file_enabled ?
  • {svg_checkmark}{translate('Enable file copy and paste')}
  • : ""} {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} {keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 0296d82bda5..d57da22670e 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -66,6 +66,39 @@ impl SciterHandler { } displays_value } + + fn make_platform_additions(data: &str) -> Option { + if let Ok(v2) = serde_json::from_str::>(data) { + let mut value = Value::map(); + for (k, v) in v2 { + match v { + serde_json::Value::String(s) => { + value.set_item(k, s); + } + serde_json::Value::Number(n) => { + if let Some(n) = n.as_i64() { + value.set_item(k, n as i32); + } else if let Some(n) = n.as_f64() { + value.set_item(k, n); + } + } + serde_json::Value::Bool(b) => { + value.set_item(k, b); + } + _ => { + // ignore for now + } + } + } + if value.len() > 0 { + return Some(value); + } else { + None + } + } else { + None + } + } } impl InvokeUiSession for SciterHandler { @@ -245,6 +278,9 @@ impl InvokeUiSession for SciterHandler { pi_sciter.set_item("displays", Self::make_displays_array(&pi.displays)); pi_sciter.set_item("current_display", pi.current_display); pi_sciter.set_item("version", pi.version.clone()); + if let Some(v) = Self::make_platform_additions(&pi.platform_additions) { + pi_sciter.set_item("platform_additions", v); + } self.call("updatePi", &make_args!(pi_sciter)); } @@ -500,6 +536,7 @@ impl sciter::EventHandler for SciterSession { fn version_cmp(String, String); fn set_selected_windows_session_id(String); fn is_recording(); + fn has_file_clipboard(); } } @@ -607,6 +644,10 @@ impl SciterSession { self.send_selected_session_id(u_sid); } + fn has_file_clipboard(&self) -> bool { + cfg!(any(target_os = "windows", feature = "unix-file-copy-paste")) + } + fn get_port_forwards(&mut self) -> Value { let port_forwards = self.lc.read().unwrap().port_forwards.clone(); let mut v = Value::array(0); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index a3373f8ccd7..c1e25362a84 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -3,8 +3,11 @@ use crate::ipc::ClipboardNonFile; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ipc::Connection; #[cfg(not(any(target_os = "ios")))] -use crate::ipc::{self, Data}; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +use crate::{ + clipboard::ClipboardSide, + ipc::{self, Data}, +}; +#[cfg(target_os = "windows")] use clipboard::ContextSend; #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::tokio::sync::mpsc::unbounded_channel; @@ -71,9 +74,9 @@ struct IpcTaskRunner { close: bool, running: bool, conn_id: i32, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] file_transfer_enabled: bool, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] file_transfer_enabled_peer: bool, } @@ -169,7 +172,7 @@ impl ConnectionManager { } #[inline] - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] fn is_authorized(&self, id: i32) -> bool { CLIENTS .read() @@ -190,12 +193,9 @@ impl ConnectionManager { .map(|c| c.disconnected = true); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] { - let _ = ContextSend::proc(|context| -> ResultType<()> { - context.empty_clipboard(id)?; - Ok(()) - }); + crate::clipboard::try_empty_clipboard_files(ClipboardSide::Host, id); } #[cfg(any(target_os = "android"))] @@ -345,31 +345,40 @@ impl IpcTaskRunner { // for tmp use, without real conn id let mut write_jobs: Vec = Vec::new(); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] let is_authorized = self.cm.is_authorized(self.conn_id); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - let rx_clip1; + #[cfg(target_os = "windows")] + let rx_clip_holder; let mut rx_clip; let _tx_clip; - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] if self.conn_id > 0 && is_authorized { log::debug!("Clipboard is enabled from client peer: type 1"); - rx_clip1 = clipboard::get_rx_cliprdr_server(self.conn_id); - rx_clip = rx_clip1.lock().await; + let conn_id = self.conn_id; + rx_clip_holder = ( + clipboard::get_rx_cliprdr_server(conn_id), + Some(crate::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + clipboard::remove_channel_by_conn_id(conn_id); + }), + }), + ); + rx_clip = rx_clip_holder.0.lock().await; } else { log::debug!("Clipboard is enabled from client peer, actually useless: type 2"); let rx_clip2; (_tx_clip, rx_clip2) = unbounded_channel::(); - rx_clip1 = Arc::new(TokioMutex::new(rx_clip2)); - rx_clip = rx_clip1.lock().await; + rx_clip_holder = (Arc::new(TokioMutex::new(rx_clip2)), None); + rx_clip = rx_clip_holder.0.lock().await; } - #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] + #[cfg(not(target_os = "windows"))] { (_tx_clip, rx_clip) = unbounded_channel::(); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] { if ContextSend::is_enabled() { log::debug!("Clipboard is enabled"); @@ -397,7 +406,7 @@ impl IpcTaskRunner { log::debug!("conn_id: {}", id); self.cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); self.conn_id = id; - #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] + #[cfg(target_os = "windows")] { self.file_transfer_enabled = _file_transfer_enabled; } @@ -438,34 +447,31 @@ impl IpcTaskRunner { Data::FileTransferLog((action, log)) => { self.cm.ui_handler.file_transfer_log(&action, &log); } - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(target_os = "windows")] Data::ClipboardFile(_clip) => { - #[cfg(any(target_os = "windows", target_os="linux", target_os = "macos"))] - { - 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); - log::debug!( - "Process clipboard message from client peer, stop: {}, is_stopping_allowed: {}, is_clipboard_enabled: {}, file_transfer_enabled: {}", - stop, is_stopping_allowed, is_clipboard_enabled, file_transfer_enabled); - if stop { - ContextSend::set_is_stopped(); - } else { - if !is_authorized { - log::debug!("Clipboard message from client peer, but not authorized"); - continue; - } - let conn_id = self.conn_id; - let _ = ContextSend::proc(|context| -> ResultType<()> { - context.server_clip_file(conn_id, _clip) - .map_err(|e| e.into()) - }); + 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); + log::debug!( + "Process clipboard message from client peer, stop: {}, is_stopping_allowed: {}, is_clipboard_enabled: {}, file_transfer_enabled: {}", + stop, is_stopping_allowed, is_clipboard_enabled, file_transfer_enabled); + if stop { + ContextSend::set_is_stopped(); + } else { + if !is_authorized { + log::debug!("Clipboard message from client peer, but not authorized"); + continue; } + let conn_id = self.conn_id; + let _ = ContextSend::proc(|context| -> ResultType<()> { + context.server_clip_file(conn_id, _clip) + .map_err(|e| e.into()) + }); } } Data::ClipboardFileEnabled(_enabled) => { - #[cfg(any(target_os= "windows",target_os ="linux", target_os = "macos"))] + #[cfg(target_os = "windows")] { self.file_transfer_enabled_peer = _enabled; } @@ -543,7 +549,7 @@ impl IpcTaskRunner { } match &data { Data::SwitchPermission{name: _name, enabled: _enabled} => { - #[cfg(any(target_os="linux", target_os="windows", target_os = "macos"))] + #[cfg(target_os = "windows")] if _name == "file" { self.file_transfer_enabled = *_enabled; } @@ -558,7 +564,7 @@ impl IpcTaskRunner { }, clip_file = rx_clip.recv() => match clip_file { Some(_clip) => { - #[cfg(any(target_os = "windows", target_os ="linux", target_os = "macos"))] + #[cfg(target_os = "windows")] { let is_stopping_allowed = _clip.is_stopping_allowed(); let is_clipboard_enabled = ContextSend::is_enabled(); @@ -602,9 +608,9 @@ impl IpcTaskRunner { close: true, running: true, conn_id: 0, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] file_transfer_enabled: false, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] file_transfer_enabled_peer: false, }; @@ -623,13 +629,7 @@ impl IpcTaskRunner { #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main(flavor = "current_thread")] pub async fn start_ipc(cm: ConnectionManager) { - #[cfg(any( - target_os = "windows", - all( - any(target_os = "linux", target_os = "macos"), - feature = "unix-file-copy-paste" - ), - ))] + #[cfg(target_os = "windows")] ContextSend::enable(option2bool( OPTION_ENABLE_FILE_TRANSFER, &Config::get_option(OPTION_ENABLE_FILE_TRANSFER), diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 323b651fedb..1aa4130deb0 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -23,7 +23,6 @@ use serde_derive::Serialize; use std::process::Child; use std::{ collections::HashMap, - sync::atomic::{AtomicUsize, Ordering}, sync::{Arc, Mutex}, }; @@ -213,6 +212,7 @@ pub fn get_local_option(key: String) -> String { } #[inline] +#[cfg(feature = "flutter")] pub fn get_hard_option(key: String) -> String { config::HARD_SETTINGS .read() @@ -491,6 +491,7 @@ pub fn set_socks(proxy: String, username: String, password: String) { } #[inline] +#[cfg(feature = "flutter")] pub fn get_proxy_status() -> bool { #[cfg(not(any(target_os = "android", target_os = "ios")))] return ipc::get_proxy_status(); @@ -1150,13 +1151,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver bool { + *self.server_keyboard_enabled.read().unwrap() + && *self.server_file_transfer_enabled.read().unwrap() + && self.lc.read().unwrap().enable_file_copy_paste.v + } } impl Session { @@ -324,7 +331,7 @@ impl Session { pub fn toggle_option(&self, name: String) { let msg = self.lc.write().unwrap().toggle_option(name.clone()); - #[cfg(not(feature = "flutter"))] + #[cfg(all(target_os = "windows", not(feature = "flutter")))] if name == hbb_common::config::keys::OPTION_ENABLE_FILE_COPY_PASTE { self.send(Data::ToggleClipboardFile); } @@ -361,6 +368,13 @@ impl Session { && !self.lc.read().unwrap().disable_clipboard.v } + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + pub fn is_file_clipboard_required(&self) -> bool { + *self.server_keyboard_enabled.read().unwrap() + && *self.server_file_transfer_enabled.read().unwrap() + && self.lc.read().unwrap().enable_file_copy_paste.v + } + #[cfg(feature = "flutter")] pub fn refresh_video(&self, display: i32) { if crate::common::is_support_multi_ui_session_num(self.lc.read().unwrap().version) { From aa63ebc7e5c130267039555a5cade5b085c15bec Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 5 Feb 2025 15:28:30 +0800 Subject: [PATCH 074/506] Misuse Disclaimer --- README.md | 4 ++++ docs/README-DE.md | 4 ++++ docs/README-ES.md | 4 ++++ docs/README-FR.md | 4 ++++ docs/README-IT.md | 4 ++++ docs/README-JP.md | 4 ++++ docs/README-KR.md | 4 ++++ docs/README-PTBR.md | 4 ++++ docs/README-RU.md | 4 ++++ docs/README-ZH.md | 4 ++++ 10 files changed, 40 insertions(+) diff --git a/README.md b/README.md index 1c4d6be4afe..3e468e0909f 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,10 @@ Please ensure that you are running these commands from the root of the RustDesk - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client +> [!Caution] +> **Misuse Disclaimer:**
    +> The developers of RustDesk do not condone or support any unethical or illegal use of this software. Misuse, such as unauthorized access, control or invasion of privacy, is strictly against our guidelines. The authors are not responsible for any misuse of the application. + ## Screenshots ![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) diff --git a/docs/README-DE.md b/docs/README-DE.md index 28e0bc19a1f..02144acdaaa 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -147,6 +147,10 @@ target/release/rustdesk Bitte stellen Sie sicher, dass Sie diese Befehle im Stammverzeichnis des RustDesk-Repositorys nutzen. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenken Sie auch, dass andere Cargo-Unterbefehle wie `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf Ihrem eigentlichen System. +> [!Vorsicht] +> **Haftungsausschluss bei Missbrauch::**
    +> Die Entwickler von RustDesk billigen oder unterstützen keine unethische oder illegale Nutzung dieser Software. Missbrauch, wie unbefugter Zugriff, unbefugte Kontrolle oder Verletzung der Privatsphäre, verstößt strikt gegen unsere Richtlinien. Die Autoren sind nicht verantwortlich für jeglichen Missbrauch der Anwendung. + ## Dateistruktur - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video-Codec, Konfiguration, TCP/UDP-Wrapper, Protokoll-Puffer, fs-Funktionen für Dateitransfer und ein paar andere nützliche Funktionen diff --git a/docs/README-ES.md b/docs/README-ES.md index dea12c15253..a78e120bb2c 100644 --- a/docs/README-ES.md +++ b/docs/README-ES.md @@ -147,6 +147,10 @@ Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para el cliente web Flutter +> [!Precaución] +> **Descargo de responsabilidad por uso indebido:**
    +> Los desarrolladores de RustDesk no aprueban ni apoyan ningún uso no ético o ilegal de este software. El uso indebido, como el acceso no autorizado, el control o la invasión de la privacidad, está estrictamente en contra de nuestras directrices. Los autores no son responsables de ningún uso indebido de la aplicación. + ## Capturas de pantalla ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-FR.md b/docs/README-FR.md index 1ad38ebc29f..996ae032c0a 100644 --- a/docs/README-FR.md +++ b/docs/README-FR.md @@ -137,6 +137,10 @@ Veuillez vous assurer que vous exécutez ces commandes à partir de la racine du - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)** : Communiquer avec [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attendre une connexion distante directe (TCP hole punching) ou relayée. - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)** : code spécifique à la plateforme +> [!Attention] +> **Avertissement contre l'utilisation abusive:**
    +> Les développeurs de RustDesk ne cautionnent ni ne soutiennent aucune utilisation non éthique ou illégale de ce logiciel. Toute utilisation abusive, telle que l'accès non autorisé, le contrôle ou l'invasion de la vie privée, est strictement contraire à nos directives. Les auteurs ne sont pas responsables de toute utilisation abusive de l'application. + ## Images ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-IT.md b/docs/README-IT.md index 4459f242352..72e53517c17 100644 --- a/docs/README-IT.md +++ b/docs/README-IT.md @@ -164,6 +164,10 @@ Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altr - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: codice Flutter per desktop e mobile - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript per client web Flutter +> [!Attenzione] +> **Dichiarazione di non responsabilità per uso improprio:**
    +> Gli sviluppatori di RustDesk non approvano né supportano alcun uso non etico o illegale di questo software. L'uso improprio, come l'accesso non autorizzato, il controllo o l'invasione della privacy, è strettamente contro le nostre linee guida. Gli autori non sono responsabili per qualsiasi uso improprio dell'applicazione. + ## Schermate ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-JP.md b/docs/README-JP.md index f1245fa05af..d0667b41b44 100644 --- a/docs/README-JP.md +++ b/docs/README-JP.md @@ -168,6 +168,10 @@ target/release/rustdesk - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップとモバイル向けのFlutterコード - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアント向けのJavaScript +> [!注意] +> **:不正使用に関する免責事項**
    +> RustDeskの開発者は、このソフトウェアの非倫理的または違法な使用を容認または支持しません。不正アクセス、不正な制御、またはプライバシーの侵害などの不正使用は、当社のガイドラインに厳密に違反します。開発者は、アプリケーションの不正使用に対して一切の責任を負いません。 + ## スクリーンショット ![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) diff --git a/docs/README-KR.md b/docs/README-KR.md index b0a8b973e2a..70ebcec2258 100644 --- a/docs/README-KR.md +++ b/docs/README-KR.md @@ -148,6 +148,10 @@ target/release/rustdesk - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 모바일용 Flutter 코드 - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter 웹 클라이언트용 자바스크립트 +> [!주의] +> **오용에 대한 면책 조항:**
    +> RustDesk의 개발자들은 이 소프트웨어의 비윤리적이거나 불법적인 사용을 용인하거나 지원하지 않습니다. 무단 접근, 제어 또는 개인정보 침해와 같은 오용은 우리의 지침을 엄격히 위반하는 것입니다. 개발자들은 애플리케이션의 오용에 대해 책임을 지지 않습니다. + ## 스냅샷 ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-PTBR.md b/docs/README-PTBR.md index 64c5ae001de..265a3fbeeaf 100644 --- a/docs/README-PTBR.md +++ b/docs/README-PTBR.md @@ -137,6 +137,10 @@ Por favor verifique que está executando estes comandos da raiz do repositório - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicação com [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguardar pela conexão remota direta (TCP hole punching) ou conexão indireta (relayed) - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico a cada plataforma +> [!Cuidadob] +> **Aviso de uso indevido:**
    +> Os desenvolvedores do RustDesk não aprovam nem apoiam qualquer uso antiético ou ilegal deste software. O uso indevido, como acesso não autorizado, controle ou invasão de privacidade, é estritamente contra nossas diretrizes. Os autores não são responsáveis por qualquer uso indevido da aplicação. + ## Screenshots ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-RU.md b/docs/README-RU.md index 60972efd355..283beb54dcb 100644 --- a/docs/README-RU.md +++ b/docs/README-RU.md @@ -148,6 +148,10 @@ target/release/rustdesk - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: свяжитесь с [rustdesk-server](https://github.com/rustdesk/rustdesk-server), дождитесь удаленного прямого (обход TCP NAT) или ретранслируемого соединения - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код +> [!Осторожно] +> **Отказ от ответственности за неправомерное использование:**
    +> Разработчики RustDesk не одобряют и не поддерживают какое-либо неэтичное или незаконное использование данного программного обеспечения. Неправомерное использование, такое как несанкционированный доступ, контроль или вторжение в частную жизнь, строго противоречит нашим правилам. Авторы не несут ответственности за любое неправомерное использование приложения. + ## Скриншоты ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-ZH.md b/docs/README-ZH.md index 4920ade6d96..dc4543f7893 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -218,6 +218,10 @@ target/release/rustdesk - **[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代码 +> [!警告] +> **免责声明:**
    +> RustDesk 的开发人员不纵容或支持任何不道德或非法的软件使用行为。滥用行为,例如未经授权的访问、控制或侵犯隐私,严格违反我们的准则。作者对应用程序的任何滥用行为概不负责。 + ## 截图 ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) From 9614bf266a7d405288b72675e455615177e07893 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 8 Feb 2025 16:03:04 +0800 Subject: [PATCH 075/506] update READEME --- docs/README-TR.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/README-TR.md b/docs/README-TR.md index 3b4b34edd7c..c686b826f89 100644 --- a/docs/README-TR.md +++ b/docs/README-TR.md @@ -166,6 +166,10 @@ Lütfen bu komutları RustDesk deposunun kökünden çalıştırdığınızdan e - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: mobil için Flutter kodu - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter web istemcisi için JavaScript +> [!Dikkat] +> **Yanlış Kullanım Uyarısı:**
    +> RustDesk geliştiricileri, bu yazılımın etik olmayan veya yasa dışı kullanımını onaylamaz veya desteklemez. Yetkisiz erişim, kontrol veya gizlilik ihlali gibi kötüye kullanımlar kesinlikle yönergelerimize aykırıdır. Yazarlar, uygulamanın herhangi bir yanlış kullanımından sorumlu değildir. + ## Ekran Görüntüleri ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) From 2a0e8c109b587d13f6cf1d2e829909f88ab171e7 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 10 Feb 2025 00:25:11 +0800 Subject: [PATCH 076/506] fix: macos, main window, dark theme, border (#10749) Signed-off-by: fufesou --- flutter/lib/main.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 3032a2321f0..5493975fa4f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -133,7 +133,8 @@ void runMainApp(bool startService) async { runApp(App()); // Set window option. - WindowOptions windowOptions = getHiddenTitleBarWindowOptions(); + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(isMainWindow: true); windowManager.waitUntilReadyToShow(windowOptions, () async { // Restore the location of the main window before window hide or show. await restoreWindowPosition(WindowType.Main); @@ -354,7 +355,10 @@ void runInstallPage() async { } WindowOptions getHiddenTitleBarWindowOptions( - {Size? size, bool center = false, bool? alwaysOnTop}) { + {bool isMainWindow = false, + Size? size, + bool center = false, + bool? alwaysOnTop}) { var defaultTitleBarStyle = TitleBarStyle.hidden; // we do not hide titlebar on win7 because of the frame overflow. if (kUseCompatibleUiMode) { @@ -363,7 +367,7 @@ WindowOptions getHiddenTitleBarWindowOptions( return WindowOptions( size: size, center: center, - backgroundColor: Colors.transparent, + backgroundColor: (isMacOS && isMainWindow) ? null : Colors.transparent, skipTaskbar: false, titleBarStyle: defaultTitleBarStyle, alwaysOnTop: alwaysOnTop, From a039741e5af79d280ec3a1797db4d07a12bf10a7 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:50:28 +0800 Subject: [PATCH 077/506] fix: win10, border (#10753) Signed-off-by: fufesou --- flutter/lib/common.dart | 58 +++++++++++++++++++ flutter/lib/consts.dart | 2 +- .../desktop/pages/file_manager_tab_page.dart | 12 ++-- .../desktop/pages/port_forward_tab_page.dart | 12 ++-- .../lib/desktop/pages/remote_tab_page.dart | 18 +++--- flutter/lib/desktop/pages/server_page.dart | 14 +++-- flutter/lib/main.dart | 5 +- 7 files changed, 94 insertions(+), 27 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 2fde813bcdb..9252698cf12 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2566,6 +2566,8 @@ bool get kUseCompatibleUiMode => isWindows && const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion); +bool get isWin10 => windowsBuildNumber.windowsVersion == WindowsTarget.w10; + class ServerConfig { late String idServer; late String relayServer; @@ -3638,3 +3640,59 @@ extension WorkaroundFreezeLinuxMint on Widget { } } } + +// Don't use `extension` here, the border looks weird if using `extension` in my test. +Widget workaroundWindowBorder(BuildContext context, Widget child) { + if (!isWin10) { + return child; + } + + final isLight = Theme.of(context).brightness == Brightness.light; + final borderColor = isLight ? Colors.black87 : Colors.grey; + final width = isLight ? 0.5 : 0.1; + + getBorderWidget(Widget child) { + return Obx(() => + (stateGlobal.isMaximized.isTrue || stateGlobal.fullscreen.isTrue) + ? Offstage() + : child); + } + + final List borders = [ + getBorderWidget(Container( + color: borderColor, + height: width + 0.1, + )) + ]; + if (kWindowType == WindowType.Main && !isLight) { + borders.addAll([ + getBorderWidget(Align( + alignment: Alignment.topLeft, + child: Container( + color: borderColor, + width: width, + ), + )), + getBorderWidget(Align( + alignment: Alignment.topRight, + child: Container( + color: borderColor, + width: width, + ), + )), + getBorderWidget(Align( + alignment: Alignment.bottomCenter, + child: Container( + color: borderColor, + height: width, + ), + )), + ]); + } + return Stack( + children: [ + child, + ...borders, + ], + ); +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 95b207826a7..9b8e45aa4ad 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -248,7 +248,7 @@ const kFullScreenEdgeSize = 0.0; const kMaximizeEdgeSize = 0.0; // Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead. const kWindowResizeEdgeSize = 5.0; -const kWindowBorderWidth = 1.0; +final kWindowBorderWidth = isWindows ? 0.0 : 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/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index cc77cdd9581..5251498895d 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -103,11 +103,13 @@ class _FileManagerTabPageState extends State { )); final tabWidget = isLinux ? buildVirtualWindowFrame(context, child) - : Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: child, - ); + : workaroundWindowBorder( + context, + Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: child, + )); return isMacOS || kUseCompatibleUiMode ? tabWidget : SubWindowDragToResizeArea( diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index f399f7cab68..9d366bcb0cf 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -118,11 +118,13 @@ class _PortForwardTabPageState extends State { backgroundColor: Theme.of(context).colorScheme.background, body: child), ) - : Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: child, - ); + : workaroundWindowBorder( + context, + Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: child, + )); return isMacOS || kUseCompatibleUiMode ? tabWidget : Obx( diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index efd437e1ff7..025e530c2ce 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -212,14 +212,16 @@ class _ConnectionTabPageState extends State { ); final tabWidget = isLinux ? buildVirtualWindowFrame(context, child) - : Obx(() => Container( - decoration: BoxDecoration( - border: Border.all( - color: MyTheme.color(context).border!, - width: stateGlobal.windowBorderWidth.value), - ), - child: child, - )); + : workaroundWindowBorder( + context, + Obx(() => Container( + decoration: BoxDecoration( + border: Border.all( + color: MyTheme.color(context).border!, + width: stateGlobal.windowBorderWidth.value), + ), + child: child, + ))); return isMacOS || kUseCompatibleUiMode ? tabWidget : Obx(() => SubWindowDragToResizeArea( diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 95d9f2c7c7d..9a93cb34f1d 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -88,12 +88,14 @@ class _DesktopServerPageState extends State ); return isLinux ? buildVirtualWindowFrame(context, body) - : Container( - decoration: BoxDecoration( - border: - Border.all(color: MyTheme.color(context).border!)), - child: body, - ); + : workaroundWindowBorder( + context, + Container( + decoration: BoxDecoration( + border: + Border.all(color: MyTheme.color(context).border!)), + child: body, + )); }, ), ); diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 5493975fa4f..b5a0af71144 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -489,9 +489,10 @@ class _AppState extends State with WidgetsBindingObserver { child = keyListenerBuilder(context, child); } if (isLinux) { - child = buildVirtualWindowFrame(context, child); + return buildVirtualWindowFrame(context, child); + } else { + return workaroundWindowBorder(context, child); } - return child; }, ), ); From 263bbfc66f4163ea8be4b7df7bc89f8da2ede822 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 12 Feb 2025 17:04:42 +0800 Subject: [PATCH 078/506] missed clear --- 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 748fa1c4043..e408e0e2fc2 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2430,6 +2430,8 @@ class CursorModel with ChangeNotifier { _x = -10000; _x = -10000; _image = null; + _firstUpdateMouseTime = null; + gotMouseControl = true; disposeImages(); _clearCache(); From 8f545491a284b15fecd6819e9e7081c9ce4a52db Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 14 Feb 2025 16:39:09 +0800 Subject: [PATCH 079/506] verify_login, but not eable yet --- src/common.rs | 24 ++++++++++++++++++++++++ src/ui.rs | 5 +++++ src/ui/index.tis | 7 ++++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index bcd7d3647db..56361a5da8f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1711,3 +1711,27 @@ pub fn get_builtin_option(key: &str) -> String { .cloned() .unwrap_or_default() } + +pub fn verify_login(raw: &str, id: &str) -> bool { + true + /* + #[cfg(debug_assertions)] + return true; + let Ok(pk) = crate::decode64("IycjQd4TmWvjjLnYd796Rd+XkK+KG+7GU1Ia7u4+vSw=") else { + return false; + }; + let Some(key) = get_pk(&pk).map(|x| sign::PublicKey(x)) else { + return false; + }; + let Ok(v) = crate::decode64(raw) else { + return false; + }; + let raw = sign::verify(&v, &key).unwrap_or_default(); + let v_str = std::str::from_utf8(&raw) + .unwrap_or_default() + .split(":") + .next() + .unwrap_or_default(); + v_str == id + */ +} diff --git a/src/ui.rs b/src/ui.rs index 27586a54fce..6bef48ba26a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -633,6 +633,10 @@ impl UI { pub fn verify2fa(&self, code: String) -> bool { verify2fa(code) } + + fn verify_login(&self, raw: String, id: String) -> bool { + crate::verify_login(&raw, &id) + } fn generate_2fa_img_src(&self, data: String) -> String { let v = qrcode_generator::to_png_to_vec(data, qrcode_generator::QrCodeEcc::Low, 128) @@ -739,6 +743,7 @@ impl sciter::EventHandler for UI { fn generate_2fa_img_src(String); fn verify2fa(String); fn check_hwcodec(); + fn verify_login(String, String); } } diff --git a/src/ui/index.tis b/src/ui/index.tis index 3e5aa8ad065..1dff3cecbb1 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -1358,7 +1358,8 @@ function logout() { } function refreshCurrentUser() { - if (!handler.get_local_option("access_token")) return; + var token = handler.get_local_option("access_token"); + if (!token) { return; } abLoading = true; abError = ""; app.update(); @@ -1370,6 +1371,10 @@ function refreshCurrentUser() { handleAbError(data.error); return; } + if (!handler.verify_login(data.verifier, token)) { + handleAbError("Please update your self-hosting server Pro to latest version"); + return; + } set_local_user_info(data); myIdMenu.update(); getAb(); From cefda0dec1c17278856cbc2f5936859b5cdb6c5d Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 15 Feb 2025 12:13:11 +0800 Subject: [PATCH 080/506] device group (#10781) 1. Rename `Group` tab to `Accessible devices` 2. Add accessible device groups at the top of search list 3. option `preset-device-group-name` and command line `--assign --device_group_name` Signed-off-by: 21pages --- flutter/assets/device_group.ttf | Bin 0 -> 2012 bytes flutter/lib/common.dart | 9 +- flutter/lib/common/hbbs/hbbs.dart | 19 ++++ flutter/lib/common/widgets/my_group.dart | 84 ++++++++++++++--- flutter/lib/common/widgets/peers_view.dart | 19 +++- .../lib/desktop/pages/connection_page.dart | 1 + flutter/lib/mobile/pages/connection_page.dart | 1 + flutter/lib/models/group_model.dart | 87 ++++++++++++++++-- flutter/lib/models/peer_model.dart | 8 ++ flutter/lib/models/peer_tab_model.dart | 4 +- flutter/pubspec.yaml | 3 + libs/hbb_common | 2 +- src/core_main.rs | 18 +++- src/hbbs_http/sync.rs | 4 + 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 + 57 files changed, 269 insertions(+), 33 deletions(-) create mode 100644 flutter/assets/device_group.ttf diff --git a/flutter/assets/device_group.ttf b/flutter/assets/device_group.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a6e42704f06e15332067635d9ce5521b90ce46ef GIT binary patch literal 2012 zcmd^ATW=dh6h1TSOJX}|ZO0C7op^1xPTDkf;#;bchCpeOA`rA`B8m`+>#Q%8?X{du zl^`B0kPxUyARa0q@x}{~2QDwD5)Tmq6;y;`~c4G z%=ykY=bV{2yR%D-h@8|UiN?0=q!9R_>)$@xTb^6+D$uwR3{oUi8UTw{q~yJ0HXM23ocR!oDqi4*V(FzEV|NcTEEk`xTw5 zqULjSk-kBE6LwcMw_ay%+Q9e&u;p5=s_cGe@R&rX6_L%F7OK$E@Dw{}?*VmU0{!2`WfWl8ECZ@?Gum#V zvO9C__UF4M&H$c3>x#6Ew=hj}v`iILGC9aX%ook1V+j_H*z7)c*cTRD*cbJseBnqe zoqdR$71v=1uzBAe~L| zFXU{5v#C(O{q7I_QHfpBXZfJt&R&?B(r@&5b~&wY-40E_C6to8`Ssv<5cfv|V?qr@ zgF{2XcSRR**dMk#p6PbEx=)IZ|2CKob&rnrM-ka?*S|aAlQ}zj=7c}k$JrSSI#@k4 z5teq4N52pp8$e371|xPLsJ|H;9uBfv@IaT#rJrfOPW?cl1PYo1Ru|?f+P~~m@GjCCj!F*k|K+_1?*^^HPvn$ooZ|R=-y1axubSa`?`l@* zJZ4K`jT%-YqcxDJ3N&M`Pr*tmz%m}77(0hNd3eXEWJJ~J2qkcJ_S5l?8~d!b|1#nY m6aSg}iNrXQn1y;!v-+B*H0mX_rg%Hnl3^|t$`yP(Ab$W9?kvs# literal 0 HcmV?d00001 diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 9252698cf12..06af89fa68d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -103,6 +103,8 @@ enum DesktopType { class IconFont { static const _family1 = 'Tabbar'; static const _family2 = 'PeerSearchbar'; + static const _family3 = 'AddressBook'; + static const _family4 = 'DeviceGroup'; IconFont._(); static const IconData max = IconData(0xe606, fontFamily: _family1); @@ -113,8 +115,11 @@ class IconFont { static const IconData menu = IconData(0xe628, fontFamily: _family1); static const IconData search = IconData(0xe6a4, fontFamily: _family2); static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2); - static const IconData addressBook = - IconData(0xe602, fontFamily: "AddressBook"); + static const IconData addressBook = IconData(0xe602, fontFamily: _family3); + static const IconData deviceGroupOutline = + IconData(0xe623, fontFamily: _family4); + static const IconData deviceGroupFill = + IconData(0xe748, fontFamily: _family4); } class ColorThemeExtension extends ThemeExtension { diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart index e189cc7b23f..a58ecf01e8d 100644 --- a/flutter/lib/common/hbbs/hbbs.dart +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -67,6 +67,7 @@ class PeerPayload { int? status; String user = ''; String user_name = ''; + String? device_group_name; String note = ''; PeerPayload.fromJson(Map json) @@ -75,6 +76,7 @@ class PeerPayload { status = json['status'], user = json['user'] ?? '', user_name = json['user_name'] ?? '', + device_group_name = json['device_group_name'] ?? '', note = json['note'] ?? ''; static Peer toPeer(PeerPayload p) { @@ -84,6 +86,7 @@ class PeerPayload { "username": p.info['username'] ?? '', "platform": _platform(p.info['os']), "hostname": p.info['device_name'], + "device_group_name": p.device_group_name, }); } @@ -265,3 +268,19 @@ class AbTag { : name = json['name'] ?? '', color = json['color'] ?? ''; } + +class DeviceGroupPayload { + String name; + + DeviceGroupPayload(this.name); + + DeviceGroupPayload.fromJson(Map json) + : name = json['name'] ?? ''; + + Map toGroupCacheJson() { + final Map map = { + 'name': name, + }; + return map; + } +} diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart index 359fbc7f721..efd1e5a2493 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -20,8 +20,11 @@ class MyGroup extends StatefulWidget { } class _MyGroupState extends State { - RxString get selectedUser => gFFI.groupModel.selectedUser; - RxString get searchUserText => gFFI.groupModel.searchUserText; + RxBool get isSelectedDeviceGroup => gFFI.groupModel.isSelectedDeviceGroup; + RxString get selectedAccessibleItemName => + gFFI.groupModel.selectedAccessibleItemName; + RxString get searchAccessibleItemNameText => + gFFI.groupModel.searchAccessibleItemNameText; static TextEditingController searchUserController = TextEditingController(); @override @@ -72,7 +75,7 @@ class _MyGroupState extends State { child: Container( width: double.infinity, height: double.infinity, - child: _buildUserContacts(), + child: _buildLeftList(), ), ) ], @@ -105,7 +108,7 @@ class _MyGroupState extends State { _buildLeftHeader(), Container( width: double.infinity, - child: _buildUserContacts(), + child: _buildLeftList(), ) ], ), @@ -130,7 +133,7 @@ class _MyGroupState extends State { child: TextField( controller: searchUserController, onChanged: (value) { - searchUserText.value = value; + searchAccessibleItemNameText.value = value; }, textAlignVertical: TextAlignVertical.center, style: TextStyle(fontSize: fontSize), @@ -150,20 +153,30 @@ class _MyGroupState extends State { ); } - Widget _buildUserContacts() { + Widget _buildLeftList() { return Obx(() { - final items = gFFI.groupModel.users.where((p0) { - if (searchUserText.isNotEmpty) { + final userItems = gFFI.groupModel.users.where((p0) { + if (searchAccessibleItemNameText.isNotEmpty) { return p0.name .toLowerCase() - .contains(searchUserText.value.toLowerCase()); + .contains(searchAccessibleItemNameText.value.toLowerCase()); + } + return true; + }).toList(); + final deviceGroupItems = gFFI.groupModel.deviceGroups.where((p0) { + if (searchAccessibleItemNameText.isNotEmpty) { + return p0.name + .toLowerCase() + .contains(searchAccessibleItemNameText.value.toLowerCase()); } return true; }).toList(); listView(bool isPortrait) => ListView.builder( shrinkWrap: isPortrait, - itemCount: items.length, - itemBuilder: (context, index) => _buildUserItem(items[index])); + itemCount: deviceGroupItems.length + userItems.length, + itemBuilder: (context, index) => index < deviceGroupItems.length + ? _buildDeviceGroupItem(deviceGroupItems[index]) + : _buildUserItem(userItems[index - deviceGroupItems.length])); var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); return Obx(() => stateGlobal.isPortrait.isFalse ? listView(false) @@ -174,14 +187,16 @@ class _MyGroupState extends State { Widget _buildUserItem(UserPayload user) { final username = user.name; return InkWell(onTap: () { - if (selectedUser.value != username) { - selectedUser.value = username; + isSelectedDeviceGroup.value = false; + if (selectedAccessibleItemName.value != username) { + selectedAccessibleItemName.value = username; } else { - selectedUser.value = ''; + selectedAccessibleItemName.value = ''; } }, child: Obx( () { - bool selected = selectedUser.value == username; + bool selected = !isSelectedDeviceGroup.value && + selectedAccessibleItemName.value == username; final isMe = username == gFFI.userModel.userName.value; final colorMe = MyTheme.color(context).me!; return Container( @@ -238,4 +253,43 @@ class _MyGroupState extends State { }, )).marginSymmetric(horizontal: 12).marginOnly(bottom: 6); } + + Widget _buildDeviceGroupItem(DeviceGroupPayload deviceGroup) { + final name = deviceGroup.name; + return InkWell(onTap: () { + isSelectedDeviceGroup.value = true; + if (selectedAccessibleItemName.value != name) { + selectedAccessibleItemName.value = name; + } else { + selectedAccessibleItemName.value = ''; + } + }, child: Obx( + () { + bool selected = isSelectedDeviceGroup.value && + selectedAccessibleItemName.value == name; + return Container( + decoration: BoxDecoration( + color: selected ? MyTheme.color(context).highlight : null, + border: Border( + bottom: BorderSide( + width: 0.7, + color: Theme.of(context).dividerColor.withOpacity(0.1))), + ), + child: Container( + child: Row( + children: [ + Container( + width: 20, + height: 20, + child: Icon(IconFont.deviceGroupOutline, + color: MyTheme.accent, size: 19), + ).marginOnly(right: 4), + Expanded(child: Text(name)), + ], + ).paddingSymmetric(vertical: 4), + ), + ); + }, + )).marginSymmetric(horizontal: 12).marginOnly(bottom: 6); + } } diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 3e34f882d1d..3393921b232 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -562,14 +562,23 @@ class MyGroupPeerView extends BasePeersView { ); static bool filter(Peer peer) { - if (gFFI.groupModel.searchUserText.isNotEmpty) { - if (!peer.loginName.contains(gFFI.groupModel.searchUserText)) { + if (gFFI.groupModel.searchAccessibleItemNameText.isNotEmpty) { + if (!peer.loginName + .contains(gFFI.groupModel.searchAccessibleItemNameText)) { return false; } } - if (gFFI.groupModel.selectedUser.isNotEmpty) { - if (gFFI.groupModel.selectedUser.value != peer.loginName) { - return false; + if (gFFI.groupModel.selectedAccessibleItemName.isNotEmpty) { + if (gFFI.groupModel.isSelectedDeviceGroup.value) { + if (gFFI.groupModel.selectedAccessibleItemName.value != + peer.device_group_name) { + return false; + } + } else { + if (gFFI.groupModel.selectedAccessibleItemName.value != + peer.loginName) { + return false; + } } } return true; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 235e2185a06..d9dc3eec4ea 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -350,6 +350,7 @@ class _ConnectionPageState extends State rdpPort: '', rdpUsername: '', loginName: '', + device_group_name: '', ); _autocompleteOpts = [emptyPeer]; } else { diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 1d83b5744c3..295310338b2 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -174,6 +174,7 @@ class _ConnectionPageState extends State { rdpPort: '', rdpUsername: '', loginName: '', + device_group_name: '', ); _autocompleteOpts = [emptyPeer]; } else { diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index b14ccd46b0e..155bf99f62c 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -12,16 +12,18 @@ import '../utils/http_service.dart' as http; class GroupModel { final RxBool groupLoading = false.obs; final RxString groupLoadError = "".obs; + final RxList deviceGroups = RxList.empty(growable: true); final RxList users = RxList.empty(growable: true); final RxList peers = RxList.empty(growable: true); - final RxString selectedUser = ''.obs; - final RxString searchUserText = ''.obs; + final RxBool isSelectedDeviceGroup = false.obs; + final RxString selectedAccessibleItemName = ''.obs; + final RxString searchAccessibleItemNameText = ''.obs; WeakReference parent; var initialized = false; var _cacheLoadOnceFlag = false; var _statusCode = 200; - bool get emtpy => users.isEmpty && peers.isEmpty; + bool get emtpy => deviceGroups.isEmpty && users.isEmpty && peers.isEmpty; late final Peers peersModel; @@ -55,6 +57,12 @@ class GroupModel { } Future _pull() async { + List tmpDeviceGroups = List.empty(growable: true); + if (!await _getDeviceGroups(tmpDeviceGroups)) { + // old hbbs doesn't support this api + // return; + } + tmpDeviceGroups.sort((a, b) => a.name.compareTo(b.name)); List tmpUsers = List.empty(growable: true); if (!await _getUsers(tmpUsers)) { return; @@ -63,6 +71,7 @@ class GroupModel { if (!await _getPeers(tmpPeers)) { return; } + deviceGroups.value = tmpDeviceGroups; // me first var index = tmpUsers .indexWhere((user) => user.name == gFFI.userModel.userName.value); @@ -71,8 +80,9 @@ class GroupModel { tmpUsers.insert(0, user); } users.value = tmpUsers; - if (!users.any((u) => u.name == selectedUser.value)) { - selectedUser.value = ''; + if (!users.any((u) => u.name == selectedAccessibleItemName.value) && + !deviceGroups.any((d) => d.name == selectedAccessibleItemName.value)) { + selectedAccessibleItemName.value = ''; } // recover online final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList(); @@ -84,6 +94,63 @@ class GroupModel { groupLoadError.value = ''; } + Future _getDeviceGroups( + List tmpDeviceGroups) async { + final api = "${await bind.mainGetApiServer()}/api/device-group/accessible"; + try { + var uri0 = Uri.parse(api); + final pageSize = 100; + var total = 0; + int current = 0; + do { + current += 1; + var uri = Uri( + scheme: uri0.scheme, + host: uri0.host, + path: uri0.path, + port: uri0.port, + queryParameters: { + 'current': current.toString(), + 'pageSize': pageSize.toString(), + }); + final resp = await http.get(uri, headers: getHttpHeaders()); + _statusCode = resp.statusCode; + Map json = + _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode); + if (json.containsKey('error')) { + throw json['error']; + } + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}'; + } + if (json.containsKey('total')) { + if (total == 0) total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final user in data) { + final u = DeviceGroupPayload.fromJson(user); + int index = tmpDeviceGroups.indexWhere((e) => e.name == u.name); + if (index < 0) { + tmpDeviceGroups.add(u); + } else { + tmpDeviceGroups[index] = u; + } + } + } + } + } + } while (current * pageSize < total); + return true; + } catch (err) { + debugPrint('get accessible device groups: $err'); + // old hbbs doesn't support this api + // groupLoadError.value = + // '${translate('pull_group_failed_tip')}: ${translate(err.toString())}'; + } + return false; + } + Future _getUsers(List tmpUsers) async { final api = "${await bind.mainGetApiServer()}/api/users"; try { @@ -225,6 +292,7 @@ class GroupModel { try { final map = ({ "access_token": bind.mainGetLocalOption(key: 'access_token'), + "device_groups": deviceGroups.map((e) => e.toGroupCacheJson()).toList(), "users": users.map((e) => e.toGroupCacheJson()).toList(), 'peers': peers.map((e) => e.toGroupCacheJson()).toList() }); @@ -244,8 +312,14 @@ class GroupModel { if (groupLoading.value) return; final data = jsonDecode(cache); if (data == null || data['access_token'] != access_token) return; + deviceGroups.clear(); users.clear(); peers.clear(); + if (data['device_groups'] is List) { + for (var u in data['device_groups']) { + deviceGroups.add(DeviceGroupPayload.fromJson(u)); + } + } if (data['users'] is List) { for (var u in data['users']) { users.add(UserPayload.fromJson(u)); @@ -263,9 +337,10 @@ class GroupModel { reset() async { groupLoadError.value = ''; + deviceGroups.clear(); users.clear(); peers.clear(); - selectedUser.value = ''; + selectedAccessibleItemName.value = ''; await bind.mainClearGroup(); } } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 7ab5a2b803e..d2a38c68b78 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -19,6 +19,7 @@ class Peer { String rdpUsername; bool online = false; String loginName; //login username + String device_group_name; bool? sameServer; String getId() { @@ -41,6 +42,7 @@ class Peer { rdpPort = json['rdpPort'] ?? '', rdpUsername = json['rdpUsername'] ?? '', loginName = json['loginName'] ?? '', + device_group_name = json['device_group_name'] ?? '', sameServer = json['same_server']; Map toJson() { @@ -57,6 +59,7 @@ class Peer { "rdpPort": rdpPort, "rdpUsername": rdpUsername, 'loginName': loginName, + 'device_group_name': device_group_name, 'same_server': sameServer, }; } @@ -83,6 +86,7 @@ class Peer { "hostname": hostname, "platform": platform, "login_name": loginName, + "device_group_name": device_group_name, }; } @@ -99,6 +103,7 @@ class Peer { required this.rdpPort, required this.rdpUsername, required this.loginName, + required this.device_group_name, this.sameServer, }); @@ -116,6 +121,7 @@ class Peer { rdpPort: '', rdpUsername: '', loginName: '', + device_group_name: '', ); bool equal(Peer other) { return id == other.id && @@ -129,6 +135,7 @@ class Peer { forceAlwaysRelay == other.forceAlwaysRelay && rdpPort == other.rdpPort && rdpUsername == other.rdpUsername && + device_group_name == other.device_group_name && loginName == other.loginName; } @@ -146,6 +153,7 @@ class Peer { rdpPort: other.rdpPort, rdpUsername: other.rdpUsername, loginName: other.loginName, + device_group_name: other.device_group_name, sameServer: other.sameServer); } diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index 83df1f05d6a..d152f7349e5 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -28,14 +28,14 @@ class PeerTabModel with ChangeNotifier { 'Favorites', 'Discovered', 'Address book', - 'Group', + 'Accessible devices', ]; static const List icons = [ Icons.access_time_filled, Icons.star, Icons.explore, IconFont.addressBook, - Icons.group, + IconFont.deviceGroupFill, ]; List isEnabled = List.from([ true, diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index c8dbfec854b..0c389e2224a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -161,6 +161,9 @@ flutter: - family: AddressBook fonts: - asset: assets/address_book.ttf + - family: DeviceGroup + fonts: + - asset: assets/device_group.ttf # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. diff --git a/libs/hbb_common b/libs/hbb_common index 97266d7c180..8b6700a33f8 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 97266d7c180feef8b43726ea6fcb4491e3fd8752 +Subproject commit 8b6700a33f8101fc0d9f0f2dcfcfeb0f53ac8f9b diff --git a/src/core_main.rs b/src/core_main.rs index 5264d5bfd1e..9745a32e34f 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -427,15 +427,26 @@ pub fn core_main() -> Option> { if pos < max { address_book_tag = Some(args[pos + 1].to_owned()); } + let mut device_group_name = None; + let pos = args + .iter() + .position(|x| x == "--device_group_name") + .unwrap_or(max); + if pos < max { + device_group_name = 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() && address_book_name.is_none() + if user_name.is_none() + && strategy_name.is_none() + && address_book_name.is_none() + && device_group_name.is_none() { println!( - "--user_name or --strategy_name or --address_book_name is required!" + "--user_name or --strategy_name or --address_book_name or --device_group_name is required!" ); } else { if let Some(name) = user_name { @@ -450,6 +461,9 @@ pub fn core_main() -> Option> { body["address_book_tag"] = serde_json::json!(name); } } + if let Some(name) = device_group_name { + body["device_group_name"] = 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), diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs index 91c18c4b206..1c8915cb31a 100644 --- a/src/hbbs_http/sync.rs +++ b/src/hbbs_http/sync.rs @@ -99,6 +99,10 @@ async fn start_hbbs_sync_async() { if !strategy_name.is_empty() { v[keys::OPTION_PRESET_STRATEGY_NAME] = json!(strategy_name); } + let device_group_name = get_builtin_option(keys::OPTION_PRESET_DEVICE_GROUP_NAME); + if !device_group_name.is_empty() { + v[keys::OPTION_PRESET_DEVICE_GROUP_NAME] = json!(device_group_name); + } match crate::post_request(url.replace("heartbeat", "sysinfo"), v.to_string(), "").await { Ok(x) => { if x == "SYSINFO_UPDATED" { diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 08eae5dbd5b..0ee1f3247b4 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index c2314377609..3938da6d480 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 469b1b4fb39..21763753dd0 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 33992a3386b..305d750ce2f 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 18bc5e6381e..f5fa048604f 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "更新客户端的粘贴板"), ("Untagged", "无标签"), ("new-version-of-{}-tip", "{} 版本更新"), + ("Accessible devices", "可访问的设备"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e36c493618b..310f2249ac8 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 7988da242e7..e2493eb5491 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index dbc6efc2d39..5aea3c39e9b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Client-Zwischenablage aktualisieren"), ("Untagged", "Unmarkiert"), ("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 2979c1aaad5..01a0c206884 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Ενημέρωση απομακρισμένου προχείρου"), ("Untagged", "Χωρίς ετικέτα"), ("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 7370c2429fb..3ca19da3c6c 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 3ad77afe590..680a10b8e9e 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Actualizar portapapeles del cliente"), ("Untagged", "Sin itiquetar"), ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index b38b55bd615..0878d8a14f0 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index efb281496df..929461d52fd 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index d1d3d47679a..816fc6c3e8e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 19b4e58a63b..e29ddf494fe 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index a5099f48761..1122078c18c 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index ba4723b8a2f..0c1e4a19ed3 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 7b149938e1a..94c51a2684f 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 7d90a3ea438..8a93ccae6bf 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 0f6657bbc34..abb1919dddd 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Aggiorna appunti client"), ("Untagged", "Senza tag"), ("new-version-of-{}-tip", "È disponibile una nuova versione di {}"), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 3a967afc6c9..78cc748fe88 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 6a2815aceac..a5bc432d8ec 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "클라이언트 클립보드 업데이트"), ("Untagged", "태그 없음"), ("new-version-of-{}-tip", "{} 의 새로운 버전이 출시되었습니다."), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 1f88ff77389..7362ea7138a 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 33db01f930a..e19badc6165 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index ae9626ff8e8..38bae71d8ec 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Atjaunināt klienta starpliktuvi"), ("Untagged", "Neatzīmēts"), ("new-version-of-{}-tip", "Ir pieejama jauna {} versija"), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index eb3564b86db..2901ba40ccc 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 90c87c32447..9674f0e57f3 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Klembord van client bijwerken"), ("Untagged", "Ongemarkeerd"), ("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index e28430f498e..968a2c0bc1d 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Uaktualnij schowek klienta"), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 13f829f77af..be332f7ae53 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index eff01dd5eee..81d736e6564 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 8bd79c18950..618de4a77ec 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index b035947d561..e428fbd16d7 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Обновить буфер обмена клиента"), ("Untagged", "Без метки"), ("new-version-of-{}-tip", "Доступна новая версия {}"), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 96c7977cca2..23d3c932dd3 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 6cfd29d6c1d..0baf7f25488 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Osveži odjemalčevo odložišče"), ("Untagged", "Neoznačeno"), ("new-version-of-{}-tip", "Na voljo je nova različica {}"), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index ad76f2f9cb2..6d121f77544 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 286658657a0..cc7a412913f 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index fcb2fe1ae41..2f1af1857ec 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 9f1293ce198..b5edd1f83d8 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 83fac2ab8b4..fd04d360535 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 630add8bb56..57c1c9254ed 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index d7eb8dc69fd..95b41989b5f 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "更新客戶端的剪貼簿"), ("Untagged", "無標籤"), ("new-version-of-{}-tip", "有新版本的 {} 可用"), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 9f0dfdefbbc..065d73fbe94 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Оновити буфер обміну клієнта"), ("Untagged", "Без міток"), ("new-version-of-{}-tip", "Доступна нова версія {}"), + ("Accessible devices", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 1ee2cd6d026..7f803bf66e0 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), ].iter().cloned().collect(); } From a548e9c94dba8747e19a10d3987d7ebac96e0f49 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 15 Feb 2025 18:33:26 +0800 Subject: [PATCH 081/506] fix: android, controlled side, gesture (#10792) Signed-off-by: fufesou --- .../com/carriez/flutter_hbb/InputService.kt | 81 ++++++++++++------- .../com/carriez/flutter_hbb/MainService.kt | 4 +- flutter/lib/common/widgets/remote_input.dart | 38 +++++++-- 3 files changed, 85 insertions(+), 38 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 f53f95d6754..b510be35bc0 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 @@ -34,9 +34,9 @@ import hbb.MessageOuterClass.KeyEvent import hbb.MessageOuterClass.KeyboardMode import hbb.KeyEventConverter -const val LIFT_DOWN = 9 -const val LIFT_MOVE = 8 -const val LIFT_UP = 10 +const val LEFT_DOWN = 9 +const val LEFT_MOVE = 8 +const val LEFT_UP = 10 const val RIGHT_UP = 18 const val WHEEL_BUTTON_DOWN = 33 const val WHEEL_BUTTON_UP = 34 @@ -65,6 +65,7 @@ class InputService : AccessibilityService() { private val logTag = "input service" private var leftIsDown = false private var touchPath = Path() + private var stroke: GestureDescription.StrokeDescription? = null private var lastTouchGestureStartTime = 0L private var mouseX = 0 private var mouseY = 0 @@ -77,6 +78,9 @@ class InputService : AccessibilityService() { private var fakeEditTextForTextStateCalculation: EditText? = null + private var lastX = 0 + private var lastY = 0 + private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) } @RequiresApi(Build.VERSION_CODES.N) @@ -84,7 +88,7 @@ class InputService : AccessibilityService() { val x = max(0, _x) val y = max(0, _y) - if (mask == 0 || mask == LIFT_MOVE) { + if (mask == 0 || mask == LEFT_MOVE) { val oldX = mouseX val oldY = mouseY mouseX = x * SCREEN_INFO.scale @@ -99,14 +103,13 @@ class InputService : AccessibilityService() { } // left button down ,was up - if (mask == LIFT_DOWN) { + if (mask == LEFT_DOWN) { isWaitingLongPress = true timer.schedule(object : TimerTask() { override fun run() { if (isWaitingLongPress) { isWaitingLongPress = false - leftIsDown = false - endGesture(mouseX, mouseY) + continueGesture(mouseX, mouseY) } } }, LONG_TAP_DELAY * 4) @@ -122,7 +125,7 @@ class InputService : AccessibilityService() { } // left up ,was down - if (mask == LIFT_UP) { + if (mask == LEFT_UP) { if (leftIsDown) { leftIsDown = false isWaitingLongPress = false @@ -242,37 +245,59 @@ class InputService : AccessibilityService() { } private fun startGesture(x: Int, y: Int) { - touchPath = Path() + touchPath.reset() touchPath.moveTo(x.toFloat(), y.toFloat()) lastTouchGestureStartTime = System.currentTimeMillis() - } - - private fun continueGesture(x: Int, y: Int) { - touchPath.lineTo(x.toFloat(), y.toFloat()) + lastX = x + lastY = y } @RequiresApi(Build.VERSION_CODES.N) - private fun endGesture(x: Int, y: Int) { + private fun doDispatchGesture(x: Int, y: Int, willContinue: Boolean) { + touchPath.lineTo(x.toFloat(), y.toFloat()) + var duration = System.currentTimeMillis() - lastTouchGestureStartTime + if (duration <= 0) { + duration = 1 + } try { - touchPath.lineTo(x.toFloat(), y.toFloat()) - var duration = System.currentTimeMillis() - lastTouchGestureStartTime - if (duration <= 0) { - duration = 1 + if (stroke == null) { + stroke = GestureDescription.StrokeDescription( + touchPath, + 0, + duration, + willContinue + ) + } else { + stroke = stroke?.continueStroke(touchPath, 0, duration, willContinue) + } + stroke?.let { + val builder = GestureDescription.Builder() + builder.addStroke(it) + Log.d(logTag, "end gesture x:$x y:$y time:$duration") + dispatchGesture(builder.build(), null, null) } - val stroke = GestureDescription.StrokeDescription( - touchPath, - 0, - duration - ) - val builder = GestureDescription.Builder() - builder.addStroke(stroke) - Log.d(logTag, "end gesture x:$x y:$y time:$duration") - dispatchGesture(builder.build(), null, null) } catch (e: Exception) { - Log.e(logTag, "endGesture error:$e") + Log.e(logTag, "doDispatchGesture, willContinue:$willContinue, error:$e") } } + @RequiresApi(Build.VERSION_CODES.N) + private fun continueGesture(x: Int, y: Int) { + doDispatchGesture(x, y, true) + touchPath.reset() + touchPath.moveTo(x.toFloat(), y.toFloat()) + lastTouchGestureStartTime = System.currentTimeMillis() + lastX = x + lastY = y + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun endGesture(x: Int, y: Int) { + doDispatchGesture(x, y, false) + touchPath.reset() + stroke = null + } + @RequiresApi(Build.VERSION_CODES.N) fun onKeyEvent(data: ByteArray) { val keyEvent = KeyEvent.parseFrom(data) 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 e9ec0975d1b..c475dd3b019 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 @@ -65,8 +65,8 @@ class MainService : Service() { @Keep @RequiresApi(Build.VERSION_CODES.N) fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) { - // turn on screen with LIFT_DOWN when screen off - if (!powerManager.isInteractive && (kind == 0 || mask == LIFT_DOWN)) { + // turn on screen with LEFT_DOWN when screen off + if (!powerManager.isInteractive && (kind == 0 || mask == LEFT_DOWN)) { if (wakeLock.isHeld) { Log.d(logTag, "Turn on Screen, WakeLock release") wakeLock.release() diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 6eb9b059470..6667bdf8026 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -187,6 +187,11 @@ class _RawTouchGestureDetectorRegionState return; } _cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch; + if (ffiModel.isPeerMobile) { + await ffi.cursorModel + .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); + await inputModel.tapDown(MouseButtons.left); + } } } @@ -204,15 +209,31 @@ class _RawTouchGestureDetectorRegionState if (lastDeviceKind != PointerDeviceKind.touch) { return; } + if (!ffi.ffiModel.isPeerMobile) { + if (handleTouch) { + final isMoved = await ffi.cursorModel + .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); + if (!isMoved) { + return; + } + } + await inputModel.tap(MouseButtons.right); + } else { + // It's better to send a message to tell the controlled device that the long press event is triggered. + // We're now using a `TimerTask` in `InputService.kt` to decide whether to trigger the long press event. + // It's not accurate and it's better to use the same detection logic in the controlling side. + } + } + + onLongPressMoveUpdate(LongPressMoveUpdateDetails d) async { + if (!ffiModel.isPeerMobile || lastDeviceKind != PointerDeviceKind.touch) { + return; + } if (handleTouch) { - final isMoved = await ffi.cursorModel - .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); - if (!isMoved) { + if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) { return; } - } - if (!ffi.ffiModel.isPeerMobile) { - await inputModel.tap(MouseButtons.right); + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } } @@ -340,7 +361,7 @@ class _RawTouchGestureDetectorRegionState ffi.cursorModel.clearRemoteWindowCoords(); } if (handleTouch) { - await inputModel.sendMouse('up', MouseButtons.left); + await inputModel.sendMouse('up', MouseButtons.left); } } @@ -432,7 +453,8 @@ class _RawTouchGestureDetectorRegionState instance ..onLongPressDown = onLongPressDown ..onLongPressUp = onLongPressUp - ..onLongPress = onLongPress; + ..onLongPress = onLongPress + ..onLongPressMoveUpdate = onLongPressMoveUpdate; }), // Customized HoldTapMoveGestureRecognizer: From 33b47dd6e3ed6b232b7e8ab66920d1d16ca2b850 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 15 Feb 2025 18:51:15 +0800 Subject: [PATCH 082/506] allow dash in id --- libs/hbb_common | 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/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/ko.rs | 2 +- src/lang/kz.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/th.rs | 2 +- src/lang/tr.rs | 2 +- src/lang/tw.rs | 2 +- src/lang/uk.rs | 2 +- src/lang/vn.rs | 2 +- 41 files changed, 41 insertions(+), 41 deletions(-) diff --git a/libs/hbb_common b/libs/hbb_common index 8b6700a33f8..f9a10eaa1fe 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 8b6700a33f8101fc0d9f0f2dcfcfeb0f53ac8f9b +Subproject commit f9a10eaa1fe0b3614232a17ffa2c8d1f8d305456 diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 0ee1f3247b4..8e7f90f1fad 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -41,7 +41,7 @@ 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, - (dash) و _ مسموح بها. اول حرف يجب ان يكون a-z او A-Z. الطول بين 6 و 16."), ("Website", "الموقع"), ("About", "عن"), ("Slogan_tip", "صنع بحب في هذا العالم الفوضوي!"), diff --git a/src/lang/be.rs b/src/lang/be.rs index 3938da6d480..c9b5a6b4932 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -41,7 +41,7 @@ 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, - (dash) і _ (падкрэсліванне). Першай павінна быць літара a-z, A-Z. Даўжыня ад 6 да 16."), ("Website", "Сайт"), ("About", "Пра праграму"), ("Slogan_tip", "Зроблена з душой у гэтым вар'яцкім свеце!"), diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 21763753dd0..af5e4c428a4 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -41,7 +41,7 @@ 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, - (dash) и _ (долна черта) са сред позволени. Първа буква следва да е a-z, A-Z. С дължина мержу 6 и 16."), ("Website", "Уебсайт"), ("About", "Относно"), ("Slogan_tip", "Направено от сърце в този хаотичен свят!"), diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 305d750ce2f..ce8aa9fcdf8 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), + ("id_change_tip", "Els caràcters admesos són: a-z, A-Z, 0-9, - (dash), _ (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", "Quant al RustDesk"), ("Slogan_tip", "Fet de tot cor dins d'aquest món caòtic!\nTraducció: Benet R. i Camps (BennyBeat)."), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index f5fa048604f..7ede944cbad 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -41,7 +41,7 @@ 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, - (dash), _ (下划线)。首字母必须是 a-z, A-Z。长度在 6 与 16 之间。"), ("Website", "网站"), ("About", "关于"), ("Slogan_tip", "在这个混乱的世界中,用心制作!"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 310f2249ac8..4b070eb2db6 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "délka mezi %min% a %max%"), ("starts with a letter", "začíná písmenem"), ("allowed characters", "povolené znaky"), - ("id_change_tip", "Použít je možné pouze znaky a-z, A-Z, 0-9 a _ (podtržítko). Dále je třeba aby začínalo písmenem a-z, A-Z. Délka mezi 6 a 16 znaky."), + ("id_change_tip", "Použít je možné pouze znaky a-z, A-Z, 0-9, - (dash) a _ (podtržítko). Dále je třeba aby začínalo písmenem a-z, A-Z. Délka mezi 6 a 16 znaky."), ("Website", "Webové stránky"), ("About", "O aplikaci"), ("Slogan_tip", "Vytvořeno srdcem v tomto chaotickém světě!"), diff --git a/src/lang/da.rs b/src/lang/da.rs index e2493eb5491..1d22ea9a087 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), + ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9, - (dash) 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", "Lavet med kærlighed i denne kaotiske verden!"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 5aea3c39e9b..75fd81a0f6b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "Länge %min% bis %max%"), ("starts with a letter", "Beginnt mit Buchstabe"), ("allowed characters", "Erlaubte Zeichen"), - ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9 und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein und die Länge zwischen 6 und 16 Zeichen betragen."), + ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9, - (dash) und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein und die Länge zwischen 6 und 16 Zeichen betragen."), ("Website", "Webseite"), ("About", "Über"), ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), diff --git a/src/lang/el.rs b/src/lang/el.rs index 01a0c206884..0e7038d293a 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -41,7 +41,7 @@ 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, - (dash) και _ (υπογράμμιση). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."), ("Website", "Ιστότοπος"), ("About", "Πληροφορίες"), ("Slogan_tip", "Φτιαγμένο με πάθος - σε έναν κόσμο που βυθίζεται στο χάος!"), diff --git a/src/lang/en.rs b/src/lang/en.rs index 2295fd05cef..5fe3016ca07 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -5,7 +5,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("connecting_status", "Connecting to the RustDesk network..."), ("not_ready_status", "Not ready. Please check your connection"), ("ID/Relay Server", "ID/Relay server"), - ("id_change_tip", "Only a-z, A-Z, 0-9 and _ (underscore) characters allowed. The first letter must be a-z, A-Z. Length between 6 and 16."), + ("id_change_tip", "Only a-z, A-Z, 0-9, - (dash) and _ (underscore) characters allowed. The first letter must be a-z, A-Z. Length between 6 and 16."), ("Slogan_tip", "Made with heart in this chaotic world!"), ("Build Date", "Build date"), ("Audio Input", "Audio input"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 3ca19da3c6c..4bcafc03f00 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), + ("id_change_tip", "Nur la signoj a-z, A-Z, 0-9, - (dash), _ (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", "Farita kun koro en ĉi tiu ĥaosa mondo!"), diff --git a/src/lang/es.rs b/src/lang/es.rs index 680a10b8e9e..245e4415758 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "de %min% a %max% de longitud"), ("starts with a letter", "comenzar con una letra"), ("allowed characters", "Caracteres permitidos"), - ("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9 e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 y 16 caracteres."), + ("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9, - (dash) e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 y 16 caracteres."), ("Website", "Sitio web"), ("About", "Acerca de"), ("Slogan_tip", "¡Hecho con corazón en este mundo caótico!"), diff --git a/src/lang/et.rs b/src/lang/et.rs index 0878d8a14f0..d5c47155d89 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Lubatud on vaid a-z, A-Z, 0-9 ja _ (alakriips) tähemärgid. Esimene täht peab olema a-z või A-Z. Pikkus vahemikus 6-16."), + ("id_change_tip", "Lubatud on vaid a-z, A-Z, 0-9, - (dash) ja _ (alakriips) tähemärgid. Esimene täht peab olema a-z või A-Z. Pikkus vahemikus 6-16."), ("Website", ""), ("About", ""), ("Slogan_tip", "Loodud südamega selles kaootilises maailmas!"), diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 929461d52fd..9b7bc9a8ead 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "%min%(e)tik %max% arteko luzera"), ("starts with a letter", "hizki batekin hasten da"), ("allowed characters", "onartutako karaktereak"), - ("id_change_tip", "Soilik a-z, A-Z, 0-9 eta _ (barra baxua) karaktereak daude onartuta. Lehen hizkia a-z, A-Z izan behar da. Luzera 6 eta 16 artekoa izan behar da."), + ("id_change_tip", "Soilik a-z, A-Z, 0-9, - (dash) eta _ (barra baxua) karaktereak daude onartuta. Lehen hizkia a-z, A-Z izan behar da. Luzera 6 eta 16 artekoa izan behar da."), ("Website", "Webgunea"), ("About", "Honi buruz"), ("Slogan_tip", "Bihotzez eginda mundu kaotiko honetan!"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index e29ddf494fe..d156e34bc82 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "longueur de %min% à %max%"), ("starts with a letter", "commence par une lettre"), ("allowed characters", "caractères autorisés"), - ("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur doit être comprise entre 6 et 16."), + ("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, - (dash), _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur doit être comprise entre 6 et 16."), ("Website", "Site Web"), ("About", "À propos de"), ("Slogan_tip", "Fait avec cœur dans ce monde chaotique !"), diff --git a/src/lang/he.rs b/src/lang/he.rs index 1122078c18c..705d3bb1309 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), + ("id_change_tip", "מותרים רק תווים a-z, A-Z, 0-9, - (dash) ו_ (קו תחתון). האות הראשונה חייבת להיות a-z, A-Z. אורך בין 6 ל-16."), ("Website", "דף הבית"), ("About", "אודות"), ("Slogan_tip", "נוצר בלב בעולם הזה הכאוטי!"), diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 0c1e4a19ed3..501da1c12a9 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "duljina %min% do %max%"), ("starts with a letter", "Počinje slovom"), ("allowed characters", "Dopušteni znakovi"), - ("id_change_tip", "Dopušteni su samo a-z, A-Z, 0-9 i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Duljina je od 6 do 16."), + ("id_change_tip", "Dopušteni su samo a-z, A-Z, 0-9, - (dash) i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Duljina je od 6 do 16."), ("Website", "Web stranica"), ("About", "O programu"), ("Slogan_tip", "Stvoren srcem u ovom kaotičnom svijetu!"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 94c51a2684f..85405b87c20 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), + ("id_change_tip", "Csak a-z, A-Z, 0-9, - (dash) 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", "Névjegy"), ("Slogan_tip", "Szenvedéllyel programozva - egy káoszba süllyedő világban!"), diff --git a/src/lang/id.rs b/src/lang/id.rs index 8a93ccae6bf..2666abad769 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "panjang %min% s/d %max%"), ("starts with a letter", "Dimulai dengan huruf"), ("allowed characters", "Karakter yang dapat digunakan"), - ("id_change_tip", "Hanya karakter a-z, A-Z, 0-9 dan _ (underscore) yang diperbolehkan. Huruf pertama harus a-z, A-Z. Panjang antara 6 dan 16."), + ("id_change_tip", "Hanya karakter a-z, A-Z, 0-9, - (dash) dan _ (underscore) yang diperbolehkan. Huruf pertama harus a-z, A-Z. Panjang antara 6 dan 16."), ("Website", "Situs Web"), ("About", "Tentang"), ("Slogan_tip", "Dibuat dengan penuh kasih sayang dalam dunia yang penuh kekacauan ini"), diff --git a/src/lang/it.rs b/src/lang/it.rs index abb1919dddd..2b0dcfdc54a 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "lunghezza da %min% a %max%"), ("starts with a letter", "inizia con una lettera"), ("allowed characters", "caratteri consentiti"), - ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (sottolineato).\nIl primo carattere deve essere a-z o A-Z.\nLa lunghezza deve essere fra 6 e 16 caratteri."), + ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9, - (dash) e _ (sottolineato).\nIl primo carattere deve essere a-z o A-Z.\nLa lunghezza deve essere fra 6 e 16 caratteri."), ("Website", "Sito web programma"), ("About", "Info programma"), ("Slogan_tip", "Realizzato con il cuore in questo mondo caotico!"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index a5bc432d8ec..8a004809c18 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -41,7 +41,7 @@ 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, - (dash), _(언더바)만 입력 가능합니다. 첫 문자는 a-z 혹은 A-Z로 시작해야 합니다. 길이는 6~16글자가 요구됩니다."), ("Website", "웹사이트"), ("About", "정보"), ("Slogan_tip", "이 혼란스러운 세상에서 마음을 담아 만들었습니다!"), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 7362ea7138a..c0a07b9d557 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 арасы."), + ("id_change_tip", "Тек a-z, A-Z, 0-9, - (dash) және _ (астынғы-сызық) таңбалары рұқсат етілген. Бірінші таңба a-z, A-Z болуы қажет. Ұзындығы 6 мен 16 арасы."), ("Website", "Web-сайт"), ("About", "Туралы"), ("Slogan_tip", ""), diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 38bae71d8ec..81230102d3f 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "garums %min% līdz %max%"), ("starts with a letter", "sākas ar burtu"), ("allowed characters", "atļautās rakstzīmes"), - ("id_change_tip", "Atļautas tikai rakstzīmes a-z, A-Z, 0-9 un _ (pasvītrojums). Pirmajam burtam ir jābūt a-z, A-Z. Garums no 6 līdz 16."), + ("id_change_tip", "Atļautas tikai rakstzīmes a-z, A-Z, 0-9, - (dash) un _ (pasvītrojums). Pirmajam burtam ir jābūt a-z, A-Z. Garums no 6 līdz 16."), ("Website", "Tīmekļa vietne"), ("About", "Par"), ("Slogan_tip", "Radīts ar sirdi šajā haotiskajā pasaulē!"), diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 2901ba40ccc..51855906b88 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understrek) er tillat. Den første bokstaven skal være a-z, A-Z. Lengde mellom 6 og 16."), + ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9, - (dash) og _ (understrek) er tillat. Den første bokstaven skal være a-z, A-Z. Lengde mellom 6 og 16."), ("Website", "Hjemmeside"), ("About", "Om"), ("Slogan_tip", ""), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 9674f0e57f3..0d175f3025a 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "lengte %min% tot %max%"), ("starts with a letter", "begint met een letter"), ("allowed characters", "toegestane tekens"), - ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), + ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, - (dash), _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), ("Website", "Website"), ("About", "Over"), ("Slogan_tip", "Met hart gemaakt in deze chaotische wereld!"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 968a2c0bc1d..a8848bc9554 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "o długości od %min% do %max%"), ("starts with a letter", "rozpoczyna się literą"), ("allowed characters", "dozwolone znaki"), - ("id_change_tip", "Nowy ID może być złożony z małych i dużych liter a-zA-z, cyfry 0-9 oraz _ (podkreślenie). Pierwszym znakiem powinna być litera a-zA-Z, a całe ID powinno składać się z 6 do 16 znaków."), + ("id_change_tip", "Nowy ID może być złożony z małych i dużych liter a-zA-z, cyfry 0-9, - (dash) oraz _ (podkreślenie). Pierwszym znakiem powinna być litera a-zA-Z, a całe ID powinno składać się z 6 do 16 znaków."), ("Website", "Strona internetowa"), ("About", "O aplikacji"), ("Slogan_tip", "Tworzone z miłością w tym pełnym chaosu świecie!"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index be332f7ae53..e1cde5c3cd4 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), + ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9, - (dash) e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Website", "Website"), ("About", "Sobre"), ("Slogan_tip", ""), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 81d736e6564..3a64e4fb091 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "tamanho %min% para %max%"), ("starts with a letter", "começa com uma letra"), ("allowed characters", "caracteres permitidos"), - ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), + ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9, - (dash) e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Website", "Website"), ("About", "Sobre"), ("Slogan_tip", "Feito de coração neste mundo caótico!"), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 618de4a77ec..b2d02ad7119 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "lungime între %min% și %max%"), ("starts with a letter", "începe cu o literă"), ("allowed characters", "caractere permise"), - ("id_change_tip", "Pot fi utilizate doar caractere a-z, A-Z, 0-9, _ (bară jos). Primul caracter trebuie să fie a-z, A-Z. Lungimea trebuie să fie între 6 și 16 caractere."), + ("id_change_tip", "Pot fi utilizate doar caractere a-z, A-Z, 0-9, - (dash), _ (bară jos). Primul caracter trebuie să fie a-z, A-Z. Lungimea trebuie să fie între 6 și 16 caractere."), ("Website", "Site web"), ("About", "Despre"), ("Slogan_tip", "Făcut din inimă în lumea aceasta haotică!"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index e428fbd16d7..a54ec9d2b68 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -41,7 +41,7 @@ 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, - (dash) и _ (подчёркивание). Первой должна быть буква a-z, A-Z. Длина от 6 до 16."), ("Website", "Сайт"), ("About", "О приложении"), ("Slogan_tip", "Сделано с душой в этом безумном мире!"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 23d3c932dd3..adbd91fcd9e 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "dĺžka medzi %min% a %max%"), ("starts with a letter", "začína písmenom"), ("allowed characters", "povolené znaky"), - ("id_change_tip", "Povolené sú len znaky a-z, A-Z, 0-9 a _ (podčiarkovník). Prvý znak musí byť a-z, A-Z. Dĺžka musí byť medzi 6 a 16 znakmi."), + ("id_change_tip", "Povolené sú len znaky a-z, A-Z, 0-9, - (dash) a _ (podčiarkovník). Prvý znak musí byť a-z, A-Z. Dĺžka musí byť medzi 6 a 16 znakmi."), ("Website", "Webová stránka"), ("About", "O RustDesk"), ("Slogan_tip", "Stvorené srdcom v tomto chaotickom svete!"), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 0baf7f25488..ada51f77c98 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), + ("id_change_tip", "Dovoljeni znaki so a-z, A-Z (brez šumnikov), 0-9, - (dash) in _. Prvi znak mora biti črka, dolžina od 6 do 16 znakov."), ("Website", "Spletna stran"), ("About", "O programu"), ("Slogan_tip", ""), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 6d121f77544..fd35d84ba61 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Lejohen Vetëm karkteret a-z,A-Z,0-9 dhe _(nënvizimet).Shkronja e parë duhet të jetë a-z, A-Z. Gjatesia midis 6 dhe 16."), + ("id_change_tip", "Lejohen Vetëm karkteret a-z,A-Z,0-9, - (dash) dhe _(nënvizimet).Shkronja e parë duhet të jetë a-z, A-Z. Gjatesia midis 6 dhe 16."), ("Website", "Faqe ëebi"), ("About", "Rreth"), ("Slogan_tip", ""), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index cc7a412913f..47d88fadb70 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Dozvoljeni su samo a-z, A-Z, 0-9 i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Dužina je od 6 do 16."), + ("id_change_tip", "Dozvoljeni su samo a-z, A-Z, 0-9, - (dash) i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Dužina je od 6 do 16."), ("Website", "Web sajt"), ("About", "O programu"), ("Slogan_tip", ""), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 2f1af1857ec..f0a20bd5e87 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Bara a-z, A-Z, 0-9 och _ (understräck) tecken är tillåtna. Den första bokstaven måste vara a-z, A-Z. Längd mellan 6 och 16."), + ("id_change_tip", "Bara a-z, A-Z, 0-9, - (dash) och _ (understräck) tecken är tillåtna. Den första bokstaven måste vara a-z, A-Z. Längd mellan 6 och 16."), ("Website", "Hemsida"), ("About", "Om"), ("Slogan_tip", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index fd04d360535..d56fda6d2a3 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -41,7 +41,7 @@ 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, - (dash) และ _ (ขีดล่าง) เท่านั้น โดยตัวอักษรขึ้นต้นจะต้องเป็น a-z หรือไม่ก็ A-Z และมีความยาวระหว่าง 6 ถึง 16 ตัวอักษร"), ("Website", "เว็บไซต์"), ("About", "เกี่ยวกับ"), ("Slogan_tip", "ทำด้วยใจ ในโลกที่วุ่นวาย!"), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 57c1c9254ed..4b4bb484f9e 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Yalnızca a-z, A-Z, 0-9 ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), + ("id_change_tip", "Yalnızca a-z, A-Z, 0-9, - (dash) ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), ("Website", "Website"), ("About", "Hakkında"), ("Slogan_tip", ""), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 95b41989b5f..e67008be0c1 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -41,7 +41,7 @@ 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, - (dash)、_ (底線)。第一個字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), ("Website", "網站"), ("About", "關於"), ("Slogan_tip", "在這個混沌的世界中用心製作!"), diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 065d73fbe94..dfe469518fa 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -41,7 +41,7 @@ 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, - (dash) і _ (підкреслення). Першою повинна бути літера a-z, A-Z. Довжина — від 6 до 16 символів"), ("Website", "Веб-сайт"), ("About", "Про застосунок"), ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 7f803bf66e0..cd7e6b7dd10 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "độ dài %min% đến %max%"), ("starts with a letter", "bắt đầu bằng một chữ"), ("allowed characters", "các ký tự cho phép"), - ("id_change_tip", "Các kí tự đuợc phép là: từ a-z, A-Z, 0-9 và _ (dấu gạch dưới). Kí tự đầu tiên phải bắt đầu từ a-z, A-Z. Độ dài kí tự từ 6 đến 16"), + ("id_change_tip", "Các kí tự đuợc phép là: từ a-z, A-Z, 0-9, - (dash) và _ (dấu gạch dưới). Kí tự đầu tiên phải bắt đầu từ a-z, A-Z. Độ dài kí tự từ 6 đến 16"), ("Website", "Trang web"), ("About", "Giới thiệu"), ("Slogan_tip", ""), From 356adbcd8c4baed8638337ea3c213e798ab05eae Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 15 Feb 2025 19:00:43 +0800 Subject: [PATCH 083/506] more about allow - --- flutter/lib/common/widgets/dialog.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 0fb8d552d7a..135d28c1d59 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -71,7 +71,7 @@ void changeIdDialog() { final rules = [ RegexValidationRule('starts with a letter', RegExp(r'^[a-zA-Z]')), LengthRangeValidationRule(6, 16), - RegexValidationRule('allowed characters', RegExp(r'^\w*$')) + RegexValidationRule('allowed characters', RegExp(r'^[\w-]*$')) ]; gFFI.dialogManager.show((setState, close, context) { From 023d46b48c1d86b7153270e3d1a8ef72741df9b5 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 16 Feb 2025 20:45:09 +0800 Subject: [PATCH 084/506] refact: android, handle right click (#10806) Signed-off-by: fufesou --- .../com/carriez/flutter_hbb/InputService.kt | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 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 b510be35bc0..058648a6533 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 @@ -19,6 +19,7 @@ import android.view.accessibility.AccessibilityEvent import android.view.ViewGroup.LayoutParams import android.view.accessibility.AccessibilityNodeInfo import android.view.KeyEvent as KeyEventAndroid +import android.view.ViewConfiguration import android.graphics.Rect import android.media.AudioManager import android.accessibilityservice.AccessibilityServiceInfo @@ -64,13 +65,15 @@ class InputService : AccessibilityService() { private val logTag = "input service" private var leftIsDown = false - private var touchPath = Path() + private val touchPath = Path() private var stroke: GestureDescription.StrokeDescription? = null private var lastTouchGestureStartTime = 0L private var mouseX = 0 private var mouseY = 0 private var timer = Timer() private var recentActionTask: TimerTask? = null + // 100(tap timeout) + 400(long press timeout) + private val longPressDuration = ViewConfiguration.getTapTimeout().toLong() + ViewConfiguration.getLongPressTimeout().toLong() private val wheelActionsQueue = LinkedList() private var isWheelActionsPolling = false @@ -102,7 +105,7 @@ class InputService : AccessibilityService() { } } - // left button down ,was up + // left button down, was up if (mask == LEFT_DOWN) { isWaitingLongPress = true timer.schedule(object : TimerTask() { @@ -112,19 +115,19 @@ class InputService : AccessibilityService() { continueGesture(mouseX, mouseY) } } - }, LONG_TAP_DELAY * 4) + }, longPressDuration) leftIsDown = true startGesture(mouseX, mouseY) return } - // left down ,was down + // left down, was down if (leftIsDown) { continueGesture(mouseX, mouseY) } - // left up ,was down + // left up, was down if (mask == LEFT_UP) { if (leftIsDown) { leftIsDown = false @@ -135,7 +138,7 @@ class InputService : AccessibilityService() { } if (mask == RIGHT_UP) { - performGlobalAction(GLOBAL_ACTION_BACK) + longPress(mouseX, mouseY) return } @@ -244,6 +247,26 @@ class InputService : AccessibilityService() { } } + @RequiresApi(Build.VERSION_CODES.N) + private fun performClick(x: Int, y: Int, duration: Long) { + val path = Path() + path.moveTo(x.toFloat(), y.toFloat()) + try { + val longPressStroke = GestureDescription.StrokeDescription(path, 0, duration) + val builder = GestureDescription.Builder() + builder.addStroke(longPressStroke) + Log.d(logTag, "performClick x:$x y:$y time:$duration") + dispatchGesture(builder.build(), null, null) + } catch (e: Exception) { + Log.e(logTag, "performClick, error:$e") + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun longPress(x: Int, y: Int) { + performClick(x, y, longPressDuration) + } + private fun startGesture(x: Int, y: Int) { touchPath.reset() touchPath.moveTo(x.toFloat(), y.toFloat()) @@ -273,7 +296,7 @@ class InputService : AccessibilityService() { stroke?.let { val builder = GestureDescription.Builder() builder.addStroke(it) - Log.d(logTag, "end gesture x:$x y:$y time:$duration") + Log.d(logTag, "doDispatchGesture x:$x y:$y time:$duration") dispatchGesture(builder.build(), null, null) } } catch (e: Exception) { From c150143d86603ff97758948445f1f7d7d6f0a5cb Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 17 Feb 2025 16:28:47 +0800 Subject: [PATCH 085/506] device_group_name in devices.py --- res/devices.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/res/devices.py b/res/devices.py index f9bf2735292..215bd6dffdd 100755 --- a/res/devices.py +++ b/res/devices.py @@ -12,6 +12,7 @@ def view( device_name=None, user_name=None, group_name=None, + device_group_name=None, offline_days=None, ): headers = {"Authorization": f"Bearer {token}"} @@ -21,6 +22,7 @@ def view( "device_name": device_name, "user_name": user_name, "group_name": group_name, + "device_group_name": device_group_name, } params = { @@ -118,7 +120,8 @@ def main(): parser.add_argument("--id", help="Device ID") parser.add_argument("--device_name", help="Device name") parser.add_argument("--user_name", help="User name") - parser.add_argument("--group_name", help="Group name") + parser.add_argument("--group_name", help="User group name") + parser.add_argument("--device_group_name", help="Device group name") parser.add_argument( "--assign_to", help="=, e.g. user_name=mike, strategy_name=test, ab=ab1, ab=ab1,tag1", @@ -138,6 +141,7 @@ def main(): args.device_name, args.user_name, args.group_name, + args.device_group_name, args.offline_days, ) From fa49c728359a20215a49f8ac1d9c7f127231a9f7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 17 Feb 2025 17:36:47 +0800 Subject: [PATCH 086/506] fix, accessible peers filter considering device group name (#10809) Signed-off-by: 21pages --- flutter/lib/common/widgets/peers_view.dart | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 3393921b232..751c6747732 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -562,21 +562,24 @@ class MyGroupPeerView extends BasePeersView { ); static bool filter(Peer peer) { - if (gFFI.groupModel.searchAccessibleItemNameText.isNotEmpty) { - if (!peer.loginName - .contains(gFFI.groupModel.searchAccessibleItemNameText)) { + final model = gFFI.groupModel; + if (model.searchAccessibleItemNameText.isNotEmpty) { + final text = model.searchAccessibleItemNameText.value; + final searchPeersOfUser = peer.loginName.contains(text) && + model.users.any((user) => user.name == peer.loginName); + final searchPeersOfDeviceGroup = peer.device_group_name.contains(text) && + model.deviceGroups.any((g) => g.name == peer.device_group_name); + if (!searchPeersOfUser && !searchPeersOfDeviceGroup) { return false; } } - if (gFFI.groupModel.selectedAccessibleItemName.isNotEmpty) { - if (gFFI.groupModel.isSelectedDeviceGroup.value) { - if (gFFI.groupModel.selectedAccessibleItemName.value != - peer.device_group_name) { + if (model.selectedAccessibleItemName.isNotEmpty) { + if (model.isSelectedDeviceGroup.value) { + if (model.selectedAccessibleItemName.value != peer.device_group_name) { return false; } } else { - if (gFFI.groupModel.selectedAccessibleItemName.value != - peer.loginName) { + if (model.selectedAccessibleItemName.value != peer.loginName) { return false; } } From 77af6c4ce10246955ec7bfa559395220a2c329c0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 18 Feb 2025 09:08:38 +0800 Subject: [PATCH 087/506] clear selected device group or user when search text changes (#10815) Signed-off-by: 21pages --- flutter/lib/common/widgets/my_group.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart index efd1e5a2493..6207a736321 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -134,6 +134,7 @@ class _MyGroupState extends State { controller: searchUserController, onChanged: (value) { searchAccessibleItemNameText.value = value; + selectedAccessibleItemName.value = ''; }, textAlignVertical: TextAlignVertical.center, style: TextStyle(fontSize: fontSize), From 6e305d4865cc1225154f51460920321e9897ecf2 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 18 Feb 2025 16:09:25 +0800 Subject: [PATCH 088/506] improve sysinfo update --- libs/hbb_common | 2 +- src/hbbs_http/sync.rs | 43 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/libs/hbb_common b/libs/hbb_common index f9a10eaa1fe..e7d210e03a7 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit f9a10eaa1fe0b3614232a17ffa2c8d1f8d305456 +Subproject commit e7d210e03a7a7b1ba10018f4c9829f07ca7ea51b diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs index 1c8915cb31a..5a073402736 100644 --- a/src/hbbs_http/sync.rs +++ b/src/hbbs_http/sync.rs @@ -7,7 +7,8 @@ use std::{ #[cfg(not(any(target_os = "ios")))] use crate::{ui_interface::get_builtin_option, Connection}; use hbb_common::{ - config::{keys, Config, LocalConfig}, + config::{self, keys, Config, LocalConfig}, + log, tokio::{self, sync::broadcast, time::Instant}, }; use serde::{Deserialize, Serialize}; @@ -58,6 +59,7 @@ async fn start_hbbs_sync_async() { let mut last_sent: Option = None; let mut info_uploaded: (bool, String, Option, String) = (false, "".to_owned(), None, "".to_owned()); + let mut sysinfo_ver = "".to_owned(); loop { tokio::select! { _ = interval.tick() => { @@ -67,7 +69,7 @@ async fn start_hbbs_sync_async() { *PRO.lock().unwrap() = false; continue; } - if hbb_common::config::option2bool("stop-service", &Config::get_option("stop-service")) { + if config::option2bool("stop-service", &Config::get_option("stop-service")) { continue; } let conns = Connection::alive_conns(); @@ -103,11 +105,38 @@ async fn start_hbbs_sync_async() { if !device_group_name.is_empty() { v[keys::OPTION_PRESET_DEVICE_GROUP_NAME] = json!(device_group_name); } - match crate::post_request(url.replace("heartbeat", "sysinfo"), v.to_string(), "").await { + let v = v.to_string(); + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(url.as_bytes()); + hasher.update(&v.as_bytes()); + let res = hasher.finalize(); + let hash = hbb_common::base64::encode(&res[..]); + let old_hash = config::Status::get("sysinfo_hash"); + let ver = config::Status::get("sysinfo_ver"); // sysinfo_ver is the version of sysinfo on server's side + if hash == old_hash { + let samever = match crate::post_request(url.replace("heartbeat", "sysinfo_ver"), "".to_owned(), "").await { + Ok(x) => { + sysinfo_ver = x.clone(); + x == ver + } + _ => { + true // if failed to get sysinfo_ver, we assume it's the same version + } + }; + if samever { + info_uploaded = (true, url.clone(), None, id.clone()); + log::info!("sysinfo not changed, skip upload"); + continue; + } + } + match crate::post_request(url.replace("heartbeat", "sysinfo"), v, "").await { Ok(x) => { if x == "SYSINFO_UPDATED" { info_uploaded = (true, url.clone(), None, id.clone()); - hbb_common::log::info!("sysinfo updated"); + log::info!("sysinfo updated"); + config::Status::set("sysinfo_hash", hash); + config::Status::set("sysinfo_ver", sysinfo_ver.clone()); *PRO.lock().unwrap() = true; } else if x == "ID_NOT_FOUND" { info_uploaded.2 = None; // next heartbeat will upload sysinfo again @@ -136,6 +165,11 @@ async fn start_hbbs_sync_async() { v["modified_at"] = json!(modified_at); if let Ok(s) = crate::post_request(url.clone(), v.to_string(), "").await { if let Ok(mut rsp) = serde_json::from_str::>(&s) { + if rsp.remove("sysinfo").is_some() { + info_uploaded.0 = false; + config::Status::set("sysinfo_hash", "".to_owned()); + log::info!("sysinfo required to forcely update"); + } if let Some(conns) = rsp.remove("disconnect") { if let Ok(conns) = serde_json::from_value::>(conns) { SENDER.lock().unwrap().send(conns).ok(); @@ -150,6 +184,7 @@ async fn start_hbbs_sync_async() { } if let Some(strategy) = rsp.remove("strategy") { if let Ok(strategy) = serde_json::from_value::(strategy) { + log::info!("strategy updated"); handle_config_options(strategy.config_options); } } From 86b327ee41d12987858888e54bf7356d0a5b81db Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 18 Feb 2025 16:18:41 +0800 Subject: [PATCH 089/506] they always forget to remove :21114 for https, so I remove for them --- src/common.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/common.rs b/src/common.rs index 56361a5da8f..9865ab85c13 100644 --- a/src/common.rs +++ b/src/common.rs @@ -898,7 +898,16 @@ pub fn get_custom_rendezvous_server(custom: String) -> String { "".to_owned() } +#[inline] pub fn get_api_server(api: String, custom: String) -> String { + let res = get_api_server_(api, custom); + if res.starts_with("https") && res.ends_with(":21114") { + return res.replace(":21114", ""); + } + res +} + +fn get_api_server_(api: String, custom: String) -> String { #[cfg(windows)] if let Ok(lic) = crate::platform::windows::get_license_from_exe_name() { if !lic.api.is_empty() { From 451b6dc65120de6d1842144bac53a5b223702591 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Tue, 18 Feb 2025 18:10:32 +0100 Subject: [PATCH 090/506] Update nl.rs (#10812) --- 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 0d175f3025a..72bac123ba6 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -656,6 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Klembord van client bijwerken"), ("Untagged", "Ongemarkeerd"), ("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"), - ("Accessible devices", ""), + ("Accessible devices", "Toegankelijke apparaten"), ].iter().cloned().collect(); } From 1ddab27c0e5cacb3eed583fa566955a2faf3625c Mon Sep 17 00:00:00 2001 From: ANB5Dev <90337467+ANB5Dev@users.noreply.github.com> Date: Tue, 18 Feb 2025 18:10:41 +0100 Subject: [PATCH 091/506] NL lang further improvements (#10813) --- src/lang/nl.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 72bac123ba6..4033fcc698f 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -508,7 +508,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Exit", "Afsluiten"), ("Open", "Open"), ("logout_tip", "Weet u zeker dat u zich wilt afmelden?"), - ("Service", "Windows-service"), + ("Service", "Achtergrondservice"), ("Start", "Start"), ("Stop", "Stop"), ("exceed_max_devices", "Het maximum aantal gecontroleerde apparaten is bereikt."), @@ -565,13 +565,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("True color (4:4:4)", "Ware kleur (4:4:4)"), ("Enable blocking user input", "Blokkeren van gebruikersinvoer inschakelen"), ("id_input_tip", "U kunt een ID, een direct IP of een domein met poort (:) invoeren. Als u toegang wilt tot een apparaat op een andere server, voeg dan een serveradres en public key toe (@?key=), bijvoorbeeld \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.Als je toegang wilt als apparaat op een openbare server, voer dan \"@public\" in, voor de openbare server is de sleutel niet nodig."), - ("privacy_mode_impl_mag_tip", "Modus 1"), - ("privacy_mode_impl_virtual_display_tip", "Modus 2"), + ("privacy_mode_impl_mag_tip", "Modus 1: Overlayscherm"), + ("privacy_mode_impl_virtual_display_tip", "Modus 2: Monitor suspend"), ("Enter privacy mode", "Privacymodus openen"), ("Exit privacy mode", "Privacymodus afsluiten"), ("idd_not_support_under_win10_2004_tip", "Het indirecte displaystuurprogramma wordt niet ondersteund. Windows 10 versie 2004 of later is vereist."), - ("input_source_1_tip", "Invoerbron 1"), - ("input_source_2_tip", "Invoerbron 2"), + ("input_source_1_tip", "Invoerbron 1: Standaard"), + ("input_source_2_tip", "Invoerbron 2: Legacy"), ("Swap control-command key", "Wissel controle-commando toets"), ("swap-left-right-mouse", "Wissel linker- en rechtermuisknop"), ("2FA code", "2FA-code"), From cccdb2f289c1d83adc7eb7ff694d2ca4a239af4e Mon Sep 17 00:00:00 2001 From: John Fowler Date: Tue, 18 Feb 2025 18:10:52 +0100 Subject: [PATCH 092/506] Update hu.rs (#10804) Translate new strings. Clarification of some translations. --- src/lang/hu.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 85405b87c20..11c9df22be6 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -19,14 +19,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recent sessions", "Legutóbbi munkamenetek"), ("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á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-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/Továbbító-kiszolgáló"), ("Import server config", "Kiszolgáló-konfiguráció importálása"), @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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, - (dash) 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."), + ("id_change_tip", "Csak a-z, A-Z, 0-9, - (kötőjel) 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", "Névjegy"), ("Slogan_tip", "Szenvedéllyel programozva - egy káoszba süllyedő világban!"), @@ -75,9 +75,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you want to enter again?", "Szeretne újra belépni?"), ("Connection Error", "Kapcsolódási hiba"), ("Error", "Hiba"), - ("Reset by the peer", "A kapcsolatot alaphelyzetbe állt"), + ("Reset by the peer", "A kapcsolatot a másik fél lezárta."), ("Connecting...", "Kapcsolódás…"), - ("Connection in progress. Please wait.", "A kapcsolódás folyamatban van. Kis türelmet."), + ("Connection in progress. Please wait.", "A kapcsolódás folyamatban van. Kérem várjon…"), ("Please try 1 minute later", "Próbálja meg 1 perc múlva"), ("Login Error", "Bejelentkezési hiba"), ("Successful", "Sikeres"), @@ -127,7 +127,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Optimize reaction time", "Gyorsan reagáló"), ("Custom", "Egyéni"), ("Show remote cursor", "Távoli kurzor megjelenítése"), - ("Show quality monitor", "Minőségi monitor megjelenítése"), + ("Show quality monitor", "Kijelző minőségének ellenőrzé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"), @@ -145,7 +145,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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, telepítse a RustDesk alkalmazást a számítógépére."), + ("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é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"), @@ -158,10 +158,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 licenc szerződés."), + ("agreement_tip", "A telepítés folytatásával automatikusan elfogadásra kerül a licensz szerződés."), ("Accept and Install", "Elfogadás és telepítés"), - ("End-user license agreement", "Végfelhasználói licenc szerződés"), - ("Generating ...", "Előállítás…"), + ("End-user license agreement", "Végfelhasználói licensz szerződés"), + ("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 amíg TCP-tunnelinget használ"), ("Listening ...", "Figyelés…"), @@ -334,8 +334,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Secure Connection", "Biztonságos kapcsolat"), + ("Insecure Connection", "Nem biztonságos kapcsolat"), ("Scale original", "Eredeti méretarány"), ("Scale adaptive", "Adaptív méretarány"), ("General", "Általános"), @@ -653,9 +653,9 @@ 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", "A kliens vágólapjának frissítése"), + ("Update client clipboard", "Az ügyfél vágólapjának frissítése"), ("Untagged", "Címkézetlen"), ("new-version-of-{}-tip", "A(z) {} új verziója"), - ("Accessible devices", ""), + ("Accessible devices", "Hozzáférhető eszközök"), ].iter().cloned().collect(); } From ee288280b38fe5d58d0095fa4e88064248eb58db Mon Sep 17 00:00:00 2001 From: XLion Date: Wed, 19 Feb 2025 01:11:08 +0800 Subject: [PATCH 093/506] Update tw.rs (#10799) --- src/lang/tw.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index e67008be0c1..0eb0b543ba9 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -41,7 +41,7 @@ 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, - (dash)、_ (底線)。第一個字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), + ("id_change_tip", "僅能使用以下字元:a-z、A-Z、0-9、 - (dash)、_ (底線)。第一個字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), ("Website", "網站"), ("About", "關於"), ("Slogan_tip", "在這個混沌的世界中用心製作!"), @@ -646,9 +646,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "繼續"), ("Invalid file name", "無效檔名"), ("one-way-file-transfer-tip", "被控端啟用了單向檔案傳輸"), - ("Authentication Required", "需要驗證驗證"), + ("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", "上傳檔案"), @@ -656,6 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "更新客戶端的剪貼簿"), ("Untagged", "無標籤"), ("new-version-of-{}-tip", "有新版本的 {} 可用"), - ("Accessible devices", ""), + ("Accessible devices", "可存取的裝置"), ].iter().cloned().collect(); } From 2217152216e4f783dc7be2fe06b139f7a3cb57d5 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Tue, 18 Feb 2025 18:11:22 +0100 Subject: [PATCH 094/506] Italian language update (#10796) --- 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 2b0dcfdc54a..43f72fd9296 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -656,6 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Aggiorna appunti client"), ("Untagged", "Senza tag"), ("new-version-of-{}-tip", "È disponibile una nuova versione di {}"), - ("Accessible devices", ""), + ("Accessible devices", "Dispositivi accessibili"), ].iter().cloned().collect(); } From ac20d2fb5690157654ddda8c303922d5481582c6 Mon Sep 17 00:00:00 2001 From: solokot Date: Tue, 18 Feb 2025 20:11:36 +0300 Subject: [PATCH 095/506] Update ru.rs (#10794) --- 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 a54ec9d2b68..5ba372b408c 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -656,6 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Обновить буфер обмена клиента"), ("Untagged", "Без метки"), ("new-version-of-{}-tip", "Доступна новая версия {}"), - ("Accessible devices", ""), + ("Accessible devices", "Доступные устройства"), ].iter().cloned().collect(); } From 9ffe516f545a39ea640a5e5a2f5477eb62f42f20 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Wed, 19 Feb 2025 01:42:15 +0100 Subject: [PATCH 096/506] Update de.rs (#10827) --- 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 75fd81a0f6b..2e4849292a8 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "Länge %min% bis %max%"), ("starts with a letter", "Beginnt mit Buchstabe"), ("allowed characters", "Erlaubte Zeichen"), - ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9, - (dash) und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein und die Länge zwischen 6 und 16 Zeichen betragen."), + ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9, - (Bindestrich) und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein und die Länge zwischen 6 und 16 Zeichen betragen."), ("Website", "Webseite"), ("About", "Über"), ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), @@ -656,6 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Client-Zwischenablage aktualisieren"), ("Untagged", "Unmarkiert"), ("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"), - ("Accessible devices", ""), + ("Erreichbare Geräte", ""), ].iter().cloned().collect(); } From 1d1e79c8028170d5f52cb411eb44c40eb91c1397 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:14:02 +0800 Subject: [PATCH 097/506] revert, peers card, sort by online status (#10829) Signed-off-by: fufesou --- flutter/lib/common/widgets/peers_view.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 751c6747732..842374a66b6 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -25,13 +25,13 @@ class PeerSortType { static const String remoteId = 'Remote ID'; static const String remoteHost = 'Remote Host'; static const String username = 'Username'; - // static const String status = 'Status'; + static const String status = 'Status'; static List values = [ PeerSortType.remoteId, PeerSortType.remoteHost, PeerSortType.username, - // PeerSortType.status + PeerSortType.status ]; } @@ -384,9 +384,9 @@ class _PeersViewState extends State<_PeersView> peers.sort((p1, p2) => p1.username.toLowerCase().compareTo(p2.username.toLowerCase())); break; - // case PeerSortType.status: - // peers.sort((p1, p2) => p1.online ? -1 : 1); - // break; + case PeerSortType.status: + peers.sort((p1, p2) => p1.online ? -1 : 1); + break; } } From 16e191f91365c3ed40b73a8289c7332cb472c98f Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 20 Feb 2025 00:24:24 +0800 Subject: [PATCH 098/506] fix de --- 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 2e4849292a8..e482085d851 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -656,6 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Client-Zwischenablage aktualisieren"), ("Untagged", "Unmarkiert"), ("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"), - ("Erreichbare Geräte", ""), + ("Accessible devices", "Erreichbare Geräte"), ].iter().cloned().collect(); } From 965cc6af2636a09d4663fbee706e179f25e60cba Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Wed, 19 Feb 2025 17:25:23 +0100 Subject: [PATCH 099/506] Update nl.rs (#10834) --- 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 4033fcc698f..425f0b5e849 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -566,12 +566,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable blocking user input", "Blokkeren van gebruikersinvoer inschakelen"), ("id_input_tip", "U kunt een ID, een direct IP of een domein met poort (:) invoeren. Als u toegang wilt tot een apparaat op een andere server, voeg dan een serveradres en public key toe (@?key=), bijvoorbeeld \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.Als je toegang wilt als apparaat op een openbare server, voer dan \"@public\" in, voor de openbare server is de sleutel niet nodig."), ("privacy_mode_impl_mag_tip", "Modus 1: Overlayscherm"), - ("privacy_mode_impl_virtual_display_tip", "Modus 2: Monitor suspend"), + ("privacy_mode_impl_virtual_display_tip", "Modus 2: Monitor slaapstand"), ("Enter privacy mode", "Privacymodus openen"), ("Exit privacy mode", "Privacymodus afsluiten"), ("idd_not_support_under_win10_2004_tip", "Het indirecte displaystuurprogramma wordt niet ondersteund. Windows 10 versie 2004 of later is vereist."), ("input_source_1_tip", "Invoerbron 1: Standaard"), - ("input_source_2_tip", "Invoerbron 2: Legacy"), + ("input_source_2_tip", "Invoerbron 2: Verouderd"), ("Swap control-command key", "Wissel controle-commando toets"), ("swap-left-right-mouse", "Wissel linker- en rechtermuisknop"), ("2FA code", "2FA-code"), From 2e89a3321026b6136f89ad222c6d1c80361b6498 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 20 Feb 2025 01:02:24 +0800 Subject: [PATCH 100/506] fix: android, back function (#10843) Signed-off-by: fufesou --- .../com/carriez/flutter_hbb/InputService.kt | 10 ++++++++++ flutter/lib/models/input_model.dart | 17 +++++++++++++++-- 2 files changed, 25 insertions(+), 2 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 058648a6533..b2a827e129a 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 @@ -35,10 +35,15 @@ import hbb.MessageOuterClass.KeyEvent import hbb.MessageOuterClass.KeyboardMode import hbb.KeyEventConverter +// const val BUTTON_UP = 2 +// const val BUTTON_BACK = 0x08 + const val LEFT_DOWN = 9 const val LEFT_MOVE = 8 const val LEFT_UP = 10 const val RIGHT_UP = 18 +// (BUTTON_BACK << 3) | BUTTON_UP +const val BACK_UP = 66 const val WHEEL_BUTTON_DOWN = 33 const val WHEEL_BUTTON_UP = 34 const val WHEEL_DOWN = 523331 @@ -142,6 +147,11 @@ class InputService : AccessibilityService() { return } + if (mask == BACK_UP) { + performGlobalAction(GLOBAL_ACTION_BACK) + return + } + // long WHEEL_BUTTON_DOWN -> GLOBAL_ACTION_RECENTS if (mask == WHEEL_BUTTON_DOWN) { timer.purge() diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index a30bb79fdbd..2e7a36fc15b 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -18,7 +18,7 @@ import '../common.dart'; import '../consts.dart'; /// Mouse button enum. -enum MouseButtons { left, right, wheel } +enum MouseButtons { left, right, wheel, back } const _kMouseEventDown = 'mousedown'; const _kMouseEventUp = 'mouseup'; @@ -155,6 +155,8 @@ extension ToString on MouseButtons { return 'right'; case MouseButtons.wheel: return 'wheel'; + case MouseButtons.back: + return 'back'; } } } @@ -1426,7 +1428,18 @@ class InputModel { } } - void onMobileBack() => tap(MouseButtons.right); + void onMobileBack() { + final minBackButtonVersion = "1.3.8"; + final peerVersion = + parent.target?.ffiModel.pi.version ?? minBackButtonVersion; + var btn = MouseButtons.back; + // For compatibility with old versions + if (versionCmp(peerVersion, minBackButtonVersion) < 0) { + btn = MouseButtons.right; + } + tap(btn); + } + void onMobileHome() => tap(MouseButtons.wheel); Future onMobileApps() async { sendMouse('down', MouseButtons.wheel); From 055b3511648db245c68618dce02f5b181d236cbf Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:53:36 +0800 Subject: [PATCH 101/506] refact: optimize, loading recent peers (#10847) Signed-off-by: fufesou --- flutter/lib/common/widgets/autocomplete.dart | 141 +++++++++++++----- .../lib/desktop/pages/connection_page.dart | 64 ++++---- flutter/lib/mobile/pages/connection_page.dart | 62 ++++---- flutter/lib/plugin/utils/dialogs.dart | 7 +- libs/hbb_common | 2 +- src/core_main.rs | 2 + src/flutter_ffi.rs | 123 ++++++++++----- src/ui/remote.rs | 3 + 8 files changed, 265 insertions(+), 139 deletions(-) diff --git a/flutter/lib/common/widgets/autocomplete.dart b/flutter/lib/common/widgets/autocomplete.dart index 978d053df4b..d7c713648ef 100644 --- a/flutter/lib/common/widgets/autocomplete.dart +++ b/flutter/lib/common/widgets/autocomplete.dart @@ -6,56 +6,123 @@ import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; -Future> getAllPeers() async { - Map recentPeers = jsonDecode(bind.mainLoadRecentPeersSync()); - Map lanPeers = jsonDecode(bind.mainLoadLanPeersSync()); - Map combinedPeers = {}; - - void mergePeers(Map peers) { - if (peers.containsKey("peers")) { - dynamic peerData = peers["peers"]; - - if (peerData is String) { - try { - peerData = jsonDecode(peerData); - } catch (e) { - print("Error decoding peers: $e"); - return; - } +class AllPeersLoader { + List peers = []; + bool hasMoreRecentPeers = false; + + bool isPeersLoading = false; + bool _isPartialPeersLoaded = false; + bool _isPeersLoaded = false; + + AllPeersLoader(); + + bool get isLoaded => _isPartialPeersLoaded || _isPeersLoaded; + + void reset() { + peers.clear(); + hasMoreRecentPeers = false; + _isPartialPeersLoaded = false; + _isPeersLoaded = false; + } + + Future getAllPeers(void Function(VoidCallback) setState) async { + if (isPeersLoading) { + return; + } + reset(); + isPeersLoading = true; + + final startTime = DateTime.now(); + await _getAllPeers(false); + if (!hasMoreRecentPeers) { + final diffTime = DateTime.now().difference(startTime).inMilliseconds; + if (diffTime < 100) { + await Future.delayed(Duration(milliseconds: diffTime)); } + setState(() { + isPeersLoading = false; + _isPeersLoaded = true; + }); + } else { + setState(() { + _isPartialPeersLoaded = true; + }); + await _getAllPeers(true); + setState(() { + isPeersLoading = false; + _isPeersLoaded = true; + }); + } + } + + Future _getAllPeers(bool getAllRecentPeers) async { + Map recentPeers = + jsonDecode(await bind.mainGetRecentPeers(getAll: getAllRecentPeers)); + Map lanPeers = jsonDecode(bind.mainLoadLanPeersSync()); + Map combinedPeers = {}; + + void mergePeers(Map peers) { + if (peers.containsKey("peers")) { + dynamic peerData = peers["peers"]; - if (peerData is List) { - for (var peer in peerData) { - if (peer is Map && peer.containsKey("id")) { - String id = peer["id"]; - if (!combinedPeers.containsKey(id)) { - combinedPeers[id] = peer; + if (peerData is String) { + try { + peerData = jsonDecode(peerData); + } catch (e) { + print("Error decoding peers: $e"); + return; + } + } + + if (peerData is List) { + for (var peer in peerData) { + if (peer is Map && peer.containsKey("id")) { + String id = peer["id"]; + if (!combinedPeers.containsKey(id)) { + combinedPeers[id] = peer; + } } } } } } - } - mergePeers(recentPeers); - mergePeers(lanPeers); - for (var p in gFFI.abModel.allPeers()) { - if (!combinedPeers.containsKey(p.id)) { - combinedPeers[p.id] = p.toJson(); + mergePeers(recentPeers); + mergePeers(lanPeers); + for (var p in gFFI.abModel.allPeers()) { + if (!combinedPeers.containsKey(p.id)) { + combinedPeers[p.id] = p.toJson(); + } } - } - for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) { - if (!combinedPeers.containsKey(p.id)) { - combinedPeers[p.id] = p.toJson(); + for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) { + if (!combinedPeers.containsKey(p.id)) { + combinedPeers[p.id] = p.toJson(); + } + } + + List parsedPeers = []; + + for (var peer in combinedPeers.values) { + parsedPeers.add(Peer.fromJson(peer)); } - } - List parsedPeers = []; + try { + final List moreRecentPeerIds = + jsonDecode(recentPeers["ids"] ?? '[]'); + hasMoreRecentPeers = false; + for (final id in moreRecentPeerIds) { + final sid = id.toString(); + if (!parsedPeers.any((element) => element.id == sid)) { + parsedPeers.add(Peer.fromJson({'id': sid})); + hasMoreRecentPeers = true; + } + } + } catch (e) { + debugPrint("Error parsing more peer ids: $e"); + } - for (var peer in combinedPeers.values) { - parsedPeers.add(Peer.fromJson(peer)); + peers = parsedPeers; } - return parsedPeers; } class AutocompletePeerTile extends StatefulWidget { diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index d9dc3eec4ea..13133716509 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -200,18 +200,20 @@ class _ConnectionPageState extends State final _idController = IDTextEditingController(); final RxBool _idInputFocused = false.obs; + final FocusNode _idFocusNode = FocusNode(); + final TextEditingController _idEditingController = TextEditingController(); bool isWindowMinimized = false; - List peers = []; - bool isPeersLoading = false; - bool isPeersLoaded = false; + AllPeersLoader allPeersLoader = AllPeersLoader(); + // https://github.com/flutter/flutter/issues/157244 Iterable _autocompleteOpts = []; @override void initState() { super.initState(); + _idFocusNode.addListener(onFocusChanged); if (_idController.text.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) async { final lastRemoteId = await bind.mainGetLastRemoteId(); @@ -230,6 +232,9 @@ class _ConnectionPageState extends State void dispose() { _idController.dispose(); windowManager.removeListener(this); + _idFocusNode.removeListener(onFocusChanged); + _idFocusNode.dispose(); + _idEditingController.dispose(); if (Get.isRegistered()) { Get.delete(); } @@ -273,6 +278,13 @@ class _ConnectionPageState extends State bind.mainOnMainWindowClose(); } + void onFocusChanged() { + _idInputFocused.value = _idFocusNode.hasFocus; + if (_idFocusNode.hasFocus && !allPeersLoader.isPeersLoading) { + allPeersLoader.getAllPeers(setState); + } + } + @override Widget build(BuildContext context) { final isOutgoingOnly = bind.isOutgoingOnly(); @@ -304,18 +316,6 @@ class _ConnectionPageState extends State connect(context, id, isFileTransfer: isFileTransfer); } - Future _fetchPeers() async { - setState(() { - isPeersLoading = true; - }); - await Future.delayed(Duration(milliseconds: 100)); - peers = await getAllPeers(); - setState(() { - isPeersLoading = false; - isPeersLoaded = true; - }); - } - /// UI for the remote ID TextField. /// Search for a peer. Widget _buildRemoteIDTextField(BuildContext context) { @@ -332,11 +332,12 @@ class _ConnectionPageState extends State Row( children: [ Expanded( - child: Autocomplete( + child: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { _autocompleteOpts = const Iterable.empty(); - } else if (peers.isEmpty && !isPeersLoaded) { + } else if (allPeersLoader.peers.isEmpty && + !allPeersLoader.isLoaded) { Peer emptyPeer = Peer( id: '', username: '', @@ -363,7 +364,7 @@ class _ConnectionPageState extends State ); } String textToFind = textEditingValue.text.toLowerCase(); - _autocompleteOpts = peers + _autocompleteOpts = allPeersLoader.peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -377,6 +378,8 @@ class _ConnectionPageState extends State } return _autocompleteOpts; }, + focusNode: _idFocusNode, + textEditingController: _idEditingController, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, @@ -385,17 +388,17 @@ class _ConnectionPageState extends State ) { fieldTextEditingController.text = _idController.text; Get.put(fieldTextEditingController); - fieldFocusNode.addListener(() async { - _idInputFocused.value = fieldFocusNode.hasFocus; - if (fieldFocusNode.hasFocus && !isPeersLoading) { - _fetchPeers(); - } - }); - final textLength = - fieldTextEditingController.value.text.length; - // select all to facilitate removing text, just following the behavior of address input of chrome - fieldTextEditingController.selection = - TextSelection(baseOffset: 0, extentOffset: textLength); + + // The listener will be added multiple times when the widget is rebuilt. + // We may need to use the `RawAutocomplete` to get the focus node. + + // Temporarily remove Selection because Selection can cause users to accidentally delete previously entered content during input. + // final textLength = + // fieldTextEditingController.value.text.length; + // // Select all to facilitate removing text, just following the behavior of address input of chrome. + // fieldTextEditingController.selection = + // TextSelection(baseOffset: 0, extentOffset: textLength); + return Obx(() => TextField( autocorrect: false, enableSuggestions: false, @@ -468,7 +471,8 @@ class _ConnectionPageState extends State maxHeight: maxHeight, maxWidth: 319, ), - child: peers.isEmpty && isPeersLoading + child: allPeersLoader.peers.isEmpty && + !allPeersLoader.isLoaded ? Container( height: 80, child: Center( diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 295310338b2..54056a00d88 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -41,10 +41,11 @@ class _ConnectionPageState extends State { final _idController = IDTextEditingController(); final RxBool _idEmpty = true.obs; - List peers = []; + final FocusNode _idFocusNode = FocusNode(); + final TextEditingController _idEditingController = TextEditingController(); + + AllPeersLoader allPeersLoader = AllPeersLoader(); - bool isPeersLoading = false; - bool isPeersLoaded = false; StreamSubscription? _uniLinksSubscription; // https://github.com/flutter/flutter/issues/157244 @@ -61,6 +62,7 @@ class _ConnectionPageState extends State { @override void initState() { super.initState(); + _idFocusNode.addListener(onFocusChanged); if (_idController.text.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) async { final lastRemoteId = await bind.mainGetLastRemoteId(); @@ -99,6 +101,13 @@ class _ConnectionPageState extends State { connect(context, id); } + void onFocusChanged() { + _idEmpty.value = _idEditingController.text.isEmpty; + if (_idFocusNode.hasFocus && !allPeersLoader.isPeersLoading) { + allPeersLoader.getAllPeers(setState); + } + } + /// UI for software update. /// If _updateUrl] is not empty, shows a button to update the software. Widget _buildUpdateUI(String updateUrl) { @@ -127,18 +136,6 @@ class _ConnectionPageState extends State { color: Colors.white, fontWeight: FontWeight.bold)))); } - Future _fetchPeers() async { - setState(() { - isPeersLoading = true; - }); - await Future.delayed(Duration(milliseconds: 100)); - peers = await getAllPeers(); - setState(() { - isPeersLoading = false; - isPeersLoaded = true; - }); - } - /// UI for the remote ID TextField. /// Search for a peer and connect to it if the id exists. Widget _buildRemoteIDTextField() { @@ -156,11 +153,12 @@ class _ConnectionPageState extends State { Expanded( child: Container( padding: const EdgeInsets.only(left: 16, right: 16), - child: Autocomplete( + child: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { _autocompleteOpts = const Iterable.empty(); - } else if (peers.isEmpty && !isPeersLoaded) { + } else if (allPeersLoader.peers.isEmpty && + !allPeersLoader.isLoaded) { Peer emptyPeer = Peer( id: '', username: '', @@ -188,7 +186,7 @@ class _ConnectionPageState extends State { } String textToFind = textEditingValue.text.toLowerCase(); - _autocompleteOpts = peers + _autocompleteOpts = allPeersLoader.peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -202,6 +200,8 @@ class _ConnectionPageState extends State { } return _autocompleteOpts; }, + focusNode: _idFocusNode, + textEditingController: _idEditingController, fieldViewBuilder: (BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, @@ -209,18 +209,14 @@ class _ConnectionPageState extends State { fieldTextEditingController.text = _idController.text; Get.put( fieldTextEditingController); - fieldFocusNode.addListener(() async { - _idEmpty.value = - fieldTextEditingController.text.isEmpty; - if (fieldFocusNode.hasFocus && !isPeersLoading) { - _fetchPeers(); - } - }); - final textLength = - fieldTextEditingController.value.text.length; - // select all to facilitate removing text, just following the behavior of address input of chrome - fieldTextEditingController.selection = TextSelection( - baseOffset: 0, extentOffset: textLength); + + // Temporarily remove Selection because Selection can cause users to accidentally delete previously entered content during input. + // final textLength = + // fieldTextEditingController.value.text.length; + // // select all to facilitate removing text, just following the behavior of address input of chrome + // fieldTextEditingController.selection = TextSelection( + // baseOffset: 0, extentOffset: textLength); + return AutoSizeTextField( controller: fieldTextEditingController, focusNode: fieldFocusNode, @@ -300,7 +296,8 @@ class _ConnectionPageState extends State { maxHeight: maxHeight, maxWidth: 320, ), - child: peers.isEmpty && isPeersLoading + child: allPeersLoader.peers.isEmpty && + !allPeersLoader.isLoaded ? Container( height: 80, child: Center( @@ -363,6 +360,9 @@ class _ConnectionPageState extends State { void dispose() { _uniLinksSubscription?.cancel(); _idController.dispose(); + _idFocusNode.removeListener(onFocusChanged); + _idFocusNode.dispose(); + _idEditingController.dispose(); if (Get.isRegistered()) { Get.delete(); } diff --git a/flutter/lib/plugin/utils/dialogs.dart b/flutter/lib/plugin/utils/dialogs.dart index f30248f7a53..c01dc368fad 100644 --- a/flutter/lib/plugin/utils/dialogs.dart +++ b/flutter/lib/plugin/utils/dialogs.dart @@ -6,12 +6,13 @@ import 'package:flutter_hbb/models/platform_model.dart'; void showPeerSelectionDialog( {bool singleSelection = false, - required Function(List) onPeersCallback}) { - final peers = bind.mainLoadRecentPeersSync(); + required Function(List) onPeersCallback}) async { + final peers = await bind.mainGetRecentPeers(getAll: true); if (peers.isEmpty) { - debugPrint("load recent peers sync failed."); + debugPrint("load recent peers failed."); return; } + Map map = jsonDecode(peers); List peersList = map['peers'] ?? []; final selected = List.empty(growable: true); diff --git a/libs/hbb_common b/libs/hbb_common index e7d210e03a7..f94753bddb1 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit e7d210e03a7a7b1ba10018f4c9829f07ca7ea51b +Subproject commit f94753bddb11a6de52e4fe7f9e490fcf8df2275a diff --git a/src/core_main.rs b/src/core_main.rs index 9745a32e34f..182a04a1629 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -169,6 +169,8 @@ pub fn core_main() -> Option> { #[cfg(not(any(target_os = "android", target_os = "ios")))] init_plugins(&args); if args.is_empty() || crate::common::is_empty_uni_link(&args[0]) { + #[cfg(windows)] + hbb_common::config::PeerConfig::preload_peers(); std::thread::spawn(move || crate::start_server(false, no_server)); } else { #[cfg(windows)] diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5c1925dfd9a..1a65658e221 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1101,44 +1101,99 @@ pub fn main_peer_exists(id: String) -> bool { peer_exists(&id) } +fn load_recent_peers( + vec_id_modified_time_path: &Vec<(String, SystemTime, std::path::PathBuf)>, + to_end: bool, + all_peers: &mut Vec>, + from: usize, +) -> usize { + let to = if to_end { + Some(vec_id_modified_time_path.len()) + } else { + None + }; + let mut peers_next = PeerConfig::batch_peers(vec_id_modified_time_path, from, to); + // There may be less peers than the batch size. + // But no need to consider this case, because it is a rare case. + let peers = peers_next.0.drain(..).map(|(id, _, p)| peer_to_map(id, p)); + all_peers.extend(peers); + peers_next.1 +} + pub fn main_load_recent_peers() { if !config::APP_DIR.read().unwrap().is_empty() { - let peers: Vec> = PeerConfig::peers(None) - .drain(..) - .map(|(id, _, p)| peer_to_map(id, p)) - .collect(); + let vec_id_modified_time_path = PeerConfig::get_vec_id_modified_time_path(&None); + if vec_id_modified_time_path.is_empty() { + return; + } - let data = HashMap::from([ - ("name", "load_recent_peers".to_owned()), - ( - "peers", - serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), - ), - ]); - let _res = flutter::push_global_event( - flutter::APP_TYPE_MAIN, - serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + let push_to_flutter = |peers| { + let data = HashMap::from([("name", "load_recent_peers".to_owned()), ("peers", peers)]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + }; + + let load_two_times = + vec_id_modified_time_path.len() > PeerConfig::BATCH_LOADING_COUNT && cfg!(target_os = "windows"); + let mut all_peers = vec![]; + if load_two_times { + let next_from = load_recent_peers(&vec_id_modified_time_path, false, &mut all_peers, 0); + push_to_flutter(serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned())); + let _ = load_recent_peers(&vec_id_modified_time_path, true, &mut all_peers, next_from); + } else { + let _ = load_recent_peers(&vec_id_modified_time_path, true, &mut all_peers, 0); + } + push_to_flutter(serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned())); + } +} + +fn get_partial_recent_peers( + vec_id_modified_time_path: Vec<(String, SystemTime, std::path::PathBuf)>, + count: usize, +) -> String { + let (peers, next_from) = PeerConfig::batch_peers( + &vec_id_modified_time_path, + 0, + Some(count.min(vec_id_modified_time_path.len())), + ); + let peer_maps: Vec<_> = peers + .into_iter() + .map(|(id, _, p)| peer_to_map(id, p)) + .collect(); + let mut data = HashMap::from([( + "peers", + serde_json::ser::to_string(&peer_maps).unwrap_or("[]".to_owned()), + )]); + if next_from < vec_id_modified_time_path.len() { + let ids: Vec<_> = vec_id_modified_time_path[next_from..] + .iter() + .map(|(id, _, _)| id.clone()) + .collect(); + data.insert( + "ids", + serde_json::ser::to_string(&ids).unwrap_or("[]".to_owned()), ); } + return serde_json::ser::to_string(&data).unwrap_or("".to_owned()); } -pub fn main_load_recent_peers_sync() -> SyncReturn { +pub fn main_get_recent_peers(get_all: bool) -> String { if !config::APP_DIR.read().unwrap().is_empty() { - let peers: Vec> = PeerConfig::peers(None) - .drain(..) - .map(|(id, _, p)| peer_to_map(id, p)) - .collect(); + let vec_id_modified_time_path = PeerConfig::get_vec_id_modified_time_path(&None); - let data = HashMap::from([ - ("name", "load_recent_peers".to_owned()), - ( - "peers", - serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), - ), - ]); - return SyncReturn(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); + let load_two_times = !get_all + && vec_id_modified_time_path.len() > PeerConfig::BATCH_LOADING_COUNT + && cfg!(target_os = "windows"); + let load_count = if load_two_times { + PeerConfig::BATCH_LOADING_COUNT + } else { + vec_id_modified_time_path.len() + }; + return get_partial_recent_peers(vec_id_modified_time_path, load_count); } - SyncReturn("".to_string()) + "".to_string() } pub fn main_load_lan_peers_sync() -> SyncReturn { @@ -1172,11 +1227,11 @@ pub fn main_load_recent_peers_for_ab(filter: String) -> String { pub fn main_load_fav_peers() { if !config::APP_DIR.read().unwrap().is_empty() { let favs = get_fav(); - let mut recent = PeerConfig::peers(None); + let mut recent = PeerConfig::peers(Some(favs.clone())); let mut lan = config::LanPeers::load() .peers .iter() - .filter(|d| recent.iter().all(|r| r.0 != d.id)) + .filter(|d| favs.contains(&d.id) && recent.iter().all(|r| r.0 != d.id)) .map(|d| { ( d.id.clone(), @@ -1195,13 +1250,7 @@ pub fn main_load_fav_peers() { recent.append(&mut lan); let peers: Vec> = recent .into_iter() - .filter_map(|(id, _, p)| { - if favs.contains(&id) { - Some(peer_to_map(id, p)) - } else { - None - } - }) + .map(|(id, _, p)| peer_to_map(id, p)) .collect(); let data = HashMap::from([ diff --git a/src/ui/remote.rs b/src/ui/remote.rs index d57da22670e..8bcc9cc1b86 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -319,6 +319,9 @@ impl InvokeUiSession for SciterHandler { ConnType::DEFAULT_CONN => { crate::keyboard::client::start_grab_loop(); } + // Left empty code from compilation. + // Please replace the code in the PR. + ConnType::VIEW_CAMERA => {} } } From 8b9a7a350605362f9c2ae587516cf2b5b2e03edb Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 20 Feb 2025 18:31:12 +0800 Subject: [PATCH 102/506] refact: optimize, ID search peers (#10853) * refact: optimize, preload peers Signed-off-by: fufesou * Update dialogs.dart --------- Signed-off-by: fufesou Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- flutter/lib/common.dart | 6 +- flutter/lib/common/widgets/autocomplete.dart | 133 ++++++++---------- flutter/lib/common/widgets/peer_card.dart | 6 +- flutter/lib/common/widgets/peer_tab_page.dart | 6 +- .../lib/desktop/pages/connection_page.dart | 18 +-- flutter/lib/mobile/pages/connection_page.dart | 19 +-- flutter/lib/models/ab_model.dart | 22 +++ flutter/lib/models/group_model.dart | 18 +++ flutter/lib/models/peer_model.dart | 11 ++ flutter/lib/plugin/utils/dialogs.dart | 7 +- libs/hbb_common | 2 +- src/flutter_ffi.rs | 90 ++++-------- 12 files changed, 170 insertions(+), 168 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 06af89fa68d..2d4084851b5 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -822,7 +822,11 @@ class OverlayDialogManager { close([res]) { _dialogs.remove(dialogTag); - dialog.complete(res); + try { + dialog.complete(res); + } catch (e) { + debugPrint("Dialog complete catch error: $e"); + } BackButtonInterceptor.removeByName(dialogTag); } diff --git a/flutter/lib/common/widgets/autocomplete.dart b/flutter/lib/common/widgets/autocomplete.dart index d7c713648ef..1ea6d47be5d 100644 --- a/flutter/lib/common/widgets/autocomplete.dart +++ b/flutter/lib/common/widgets/autocomplete.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import '../../../models/platform_model.dart'; @@ -8,87 +7,56 @@ import 'package:flutter_hbb/common/widgets/peer_card.dart'; class AllPeersLoader { List peers = []; - bool hasMoreRecentPeers = false; bool isPeersLoading = false; - bool _isPartialPeersLoaded = false; - bool _isPeersLoaded = false; + bool isPeersLoaded = false; + + final String _listenerKey = 'AllPeersLoader'; + + late void Function(VoidCallback) setState; AllPeersLoader(); - bool get isLoaded => _isPartialPeersLoaded || _isPeersLoaded; + void init(void Function(VoidCallback) setState) { + this.setState = setState; + gFFI.recentPeersModel.addListener(_mergeAllPeers); + gFFI.lanPeersModel.addListener(_mergeAllPeers); + gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers); + gFFI.groupModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers); + } - void reset() { - peers.clear(); - hasMoreRecentPeers = false; - _isPartialPeersLoaded = false; - _isPeersLoaded = false; + void clear() { + gFFI.recentPeersModel.removeListener(_mergeAllPeers); + gFFI.lanPeersModel.removeListener(_mergeAllPeers); + gFFI.abModel.removePeerUpdateListener(_listenerKey); + gFFI.groupModel.removePeerUpdateListener(_listenerKey); } - Future getAllPeers(void Function(VoidCallback) setState) async { - if (isPeersLoading) { + Future getAllPeers() async { + if (isPeersLoaded || isPeersLoading) { return; } - reset(); isPeersLoading = true; + if (gFFI.recentPeersModel.peers.isEmpty) { + bind.mainLoadRecentPeers(); + } + if (gFFI.lanPeersModel.peers.isEmpty) { + bind.mainLoadLanPeers(); + } + // No need to care about peers from abModel, and group model. + // Because they will pull data in `refreshCurrentUser()` on startup. + final startTime = DateTime.now(); - await _getAllPeers(false); - if (!hasMoreRecentPeers) { - final diffTime = DateTime.now().difference(startTime).inMilliseconds; - if (diffTime < 100) { - await Future.delayed(Duration(milliseconds: diffTime)); - } - setState(() { - isPeersLoading = false; - _isPeersLoaded = true; - }); - } else { - setState(() { - _isPartialPeersLoaded = true; - }); - await _getAllPeers(true); - setState(() { - isPeersLoading = false; - _isPeersLoaded = true; - }); + _mergeAllPeers(); + final diffTime = DateTime.now().difference(startTime).inMilliseconds; + if (diffTime < 100) { + await Future.delayed(Duration(milliseconds: diffTime)); } } - Future _getAllPeers(bool getAllRecentPeers) async { - Map recentPeers = - jsonDecode(await bind.mainGetRecentPeers(getAll: getAllRecentPeers)); - Map lanPeers = jsonDecode(bind.mainLoadLanPeersSync()); + void _mergeAllPeers() { Map combinedPeers = {}; - - void mergePeers(Map peers) { - if (peers.containsKey("peers")) { - dynamic peerData = peers["peers"]; - - if (peerData is String) { - try { - peerData = jsonDecode(peerData); - } catch (e) { - print("Error decoding peers: $e"); - return; - } - } - - if (peerData is List) { - for (var peer in peerData) { - if (peer is Map && peer.containsKey("id")) { - String id = peer["id"]; - if (!combinedPeers.containsKey(id)) { - combinedPeers[id] = peer; - } - } - } - } - } - } - - mergePeers(recentPeers); - mergePeers(lanPeers); for (var p in gFFI.abModel.allPeers()) { if (!combinedPeers.containsKey(p.id)) { combinedPeers[p.id] = p.toJson(); @@ -101,27 +69,36 @@ class AllPeersLoader { } List parsedPeers = []; - for (var peer in combinedPeers.values) { parsedPeers.add(Peer.fromJson(peer)); } - try { - final List moreRecentPeerIds = - jsonDecode(recentPeers["ids"] ?? '[]'); - hasMoreRecentPeers = false; - for (final id in moreRecentPeerIds) { - final sid = id.toString(); - if (!parsedPeers.any((element) => element.id == sid)) { - parsedPeers.add(Peer.fromJson({'id': sid})); - hasMoreRecentPeers = true; - } + Set peerIds = combinedPeers.keys.toSet(); + for (final peer in gFFI.lanPeersModel.peers) { + if (!peerIds.contains(peer.id)) { + parsedPeers.add(peer); + peerIds.add(peer.id); + } + } + + for (final peer in gFFI.recentPeersModel.peers) { + if (!peerIds.contains(peer.id)) { + parsedPeers.add(peer); + peerIds.add(peer.id); + } + } + for (final id in gFFI.recentPeersModel.restPeerIds) { + if (!peerIds.contains(id)) { + parsedPeers.add(Peer.fromJson({'id': id})); + peerIds.add(id); } - } catch (e) { - debugPrint("Error parsing more peer ids: $e"); } peers = parsedPeers; + setState(() { + isPeersLoading = false; + isPeersLoaded = true; + }); } } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index b4bca12a9e6..6d00edc8d72 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -716,18 +716,18 @@ abstract class BasePeerCard extends StatelessWidget { switch (tab) { case PeerTabIndex.recent: await bind.mainRemovePeer(id: id); - await bind.mainLoadRecentPeers(); + bind.mainLoadRecentPeers(); break; case PeerTabIndex.fav: final favs = (await bind.mainGetFav()).toList(); if (favs.remove(id)) { await bind.mainStoreFav(favs: favs); - await bind.mainLoadFavPeers(); + bind.mainLoadFavPeers(); } break; case PeerTabIndex.lan: await bind.mainRemoveDiscovered(id: id); - await bind.mainLoadLanPeers(); + bind.mainLoadLanPeers(); break; case PeerTabIndex.ab: await gFFI.abModel.deletePeers([id]); diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 9d21ec6cd71..53c9dce06dd 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -404,7 +404,7 @@ class _PeerTabPageState extends State for (var p in peers) { await bind.mainRemovePeer(id: p.id); } - await bind.mainLoadRecentPeers(); + bind.mainLoadRecentPeers(); break; case 1: final favs = (await bind.mainGetFav()).toList(); @@ -412,13 +412,13 @@ class _PeerTabPageState extends State favs.remove(p.id); }).toList(); await bind.mainStoreFav(favs: favs); - await bind.mainLoadFavPeers(); + bind.mainLoadFavPeers(); break; case 2: for (var p in peers) { await bind.mainRemoveDiscovered(id: p.id); } - await bind.mainLoadLanPeers(); + bind.mainLoadLanPeers(); break; case 3: await gFFI.abModel.deletePeers(peers.map((p) => p.id).toList()); diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 13133716509..2dc387067d1 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -205,7 +205,7 @@ class _ConnectionPageState extends State bool isWindowMinimized = false; - AllPeersLoader allPeersLoader = AllPeersLoader(); + final AllPeersLoader _allPeersLoader = AllPeersLoader(); // https://github.com/flutter/flutter/issues/157244 Iterable _autocompleteOpts = []; @@ -213,6 +213,7 @@ class _ConnectionPageState extends State @override void initState() { super.initState(); + _allPeersLoader.init(setState); _idFocusNode.addListener(onFocusChanged); if (_idController.text.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) async { @@ -232,6 +233,7 @@ class _ConnectionPageState extends State void dispose() { _idController.dispose(); windowManager.removeListener(this); + _allPeersLoader.clear(); _idFocusNode.removeListener(onFocusChanged); _idFocusNode.dispose(); _idEditingController.dispose(); @@ -280,8 +282,8 @@ class _ConnectionPageState extends State void onFocusChanged() { _idInputFocused.value = _idFocusNode.hasFocus; - if (_idFocusNode.hasFocus && !allPeersLoader.isPeersLoading) { - allPeersLoader.getAllPeers(setState); + if (_idFocusNode.hasFocus && !_allPeersLoader.isPeersLoading) { + _allPeersLoader.getAllPeers(); } } @@ -336,8 +338,8 @@ class _ConnectionPageState extends State optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { _autocompleteOpts = const Iterable.empty(); - } else if (allPeersLoader.peers.isEmpty && - !allPeersLoader.isLoaded) { + } else if (_allPeersLoader.peers.isEmpty && + !_allPeersLoader.isPeersLoaded) { Peer emptyPeer = Peer( id: '', username: '', @@ -364,7 +366,7 @@ class _ConnectionPageState extends State ); } String textToFind = textEditingValue.text.toLowerCase(); - _autocompleteOpts = allPeersLoader.peers + _autocompleteOpts = _allPeersLoader.peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -471,8 +473,8 @@ class _ConnectionPageState extends State maxHeight: maxHeight, maxWidth: 319, ), - child: allPeersLoader.peers.isEmpty && - !allPeersLoader.isLoaded + child: _allPeersLoader.peers.isEmpty && + !_allPeersLoader.isPeersLoaded ? Container( height: 80, child: Center( diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 54056a00d88..e550200bfa1 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -44,7 +44,7 @@ class _ConnectionPageState extends State { final FocusNode _idFocusNode = FocusNode(); final TextEditingController _idEditingController = TextEditingController(); - AllPeersLoader allPeersLoader = AllPeersLoader(); + final AllPeersLoader _allPeersLoader = AllPeersLoader(); StreamSubscription? _uniLinksSubscription; @@ -62,6 +62,7 @@ class _ConnectionPageState extends State { @override void initState() { super.initState(); + _allPeersLoader.init(setState); _idFocusNode.addListener(onFocusChanged); if (_idController.text.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) async { @@ -103,8 +104,8 @@ class _ConnectionPageState extends State { void onFocusChanged() { _idEmpty.value = _idEditingController.text.isEmpty; - if (_idFocusNode.hasFocus && !allPeersLoader.isPeersLoading) { - allPeersLoader.getAllPeers(setState); + if (_idFocusNode.hasFocus && !_allPeersLoader.isPeersLoading) { + _allPeersLoader.getAllPeers(); } } @@ -157,8 +158,8 @@ class _ConnectionPageState extends State { optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { _autocompleteOpts = const Iterable.empty(); - } else if (allPeersLoader.peers.isEmpty && - !allPeersLoader.isLoaded) { + } else if (_allPeersLoader.peers.isEmpty && + !_allPeersLoader.isPeersLoaded) { Peer emptyPeer = Peer( id: '', username: '', @@ -186,7 +187,7 @@ class _ConnectionPageState extends State { } String textToFind = textEditingValue.text.toLowerCase(); - _autocompleteOpts = allPeersLoader.peers + _autocompleteOpts = _allPeersLoader.peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -296,8 +297,9 @@ class _ConnectionPageState extends State { maxHeight: maxHeight, maxWidth: 320, ), - child: allPeersLoader.peers.isEmpty && - !allPeersLoader.isLoaded + child: _allPeersLoader + .peers.isEmpty && + !_allPeersLoader.isPeersLoaded ? Container( height: 80, child: Center( @@ -361,6 +363,7 @@ class _ConnectionPageState extends State { _uniLinksSubscription?.cancel(); _idController.dispose(); _idFocusNode.removeListener(onFocusChanged); + _allPeersLoader.clear(); _idFocusNode.dispose(); _idEditingController.dispose(); if (Get.isRegistered()) { diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 3aa722a5abf..5efbd9e5fde 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -58,6 +58,9 @@ class AbModel { String? _personalAbGuid; RxBool legacyMode = false.obs; + // Only handles peers add/remove + final Map _peerIdUpdateListeners = {}; + final sortTags = shouldSortTags().obs; final filterByIntersection = filterAbTagByIntersection().obs; @@ -188,6 +191,7 @@ class AbModel { debugPrint("pull current Ab error: $e"); } } + _callbackPeerUpdate(); if (listInitialized && current.initialized) { _saveCache(); } @@ -419,6 +423,7 @@ class AbModel { } }); } + _callbackPeerUpdate(); return ret; } @@ -620,6 +625,9 @@ class AbModel { } } } + if (abEntries.isNotEmpty) { + _callbackPeerUpdate(); + } } } @@ -742,6 +750,20 @@ class AbModel { } } + void _callbackPeerUpdate() { + for (var listener in _peerIdUpdateListeners.values) { + listener(); + } + } + + void addPeerUpdateListener(String key, VoidCallback listener) { + _peerIdUpdateListeners[key] = listener; + } + + void removePeerUpdateListener(String key) { + _peerIdUpdateListeners.remove(key); + } + // #endregion } diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index 155bf99f62c..67947f5a8ab 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -23,6 +23,8 @@ class GroupModel { var _cacheLoadOnceFlag = false; var _statusCode = 200; + final Map _peerIdUpdateListeners = {}; + bool get emtpy => deviceGroups.isEmpty && users.isEmpty && peers.isEmpty; late final Peers peersModel; @@ -92,6 +94,7 @@ class GroupModel { .map((e) => e.online = true) .toList(); groupLoadError.value = ''; + _callbackPeerUpdate(); } Future _getDeviceGroups( @@ -329,6 +332,7 @@ class GroupModel { for (final peer in data['peers']) { peers.add(Peer.fromJson(peer)); } + _callbackPeerUpdate(); } } catch (e) { debugPrint("load group cache: $e"); @@ -343,4 +347,18 @@ class GroupModel { selectedAccessibleItemName.value = ''; await bind.mainClearGroup(); } + + void _callbackPeerUpdate() { + for (var listener in _peerIdUpdateListeners.values) { + listener(); + } + } + + void addPeerUpdateListener(String key, VoidCallback listener) { + _peerIdUpdateListeners[key] = listener; + } + + void removePeerUpdateListener(String key) { + _peerIdUpdateListeners.remove(key); + } } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index d2a38c68b78..35236dd4c50 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -165,6 +165,11 @@ class Peers extends ChangeNotifier { final String name; final String loadEvent; List peers = List.empty(growable: true); + // Part of the peers that are not in the rest peers list. + // When there're too many peers, we may want to load the front 100 peers first, + // so we can see peers in UI quickly. `restPeerIds` is the rest peers' ids. + // And then load all peers later. + List restPeerIds = List.empty(growable: true); final GetInitPeers? getInitPeers; UpdateEvent event = UpdateEvent.load; static const _cbQueryOnlines = 'callback_query_onlines'; @@ -238,6 +243,12 @@ class Peers extends ChangeNotifier { } else { peers = _decodePeers(evt['peers']); } + + restPeerIds = []; + if (evt['ids'] != null) { + restPeerIds = (evt['ids'] as String).split(','); + } + for (var peer in peers) { final state = onlineStates[peer.id]; peer.online = state != null && state != false; diff --git a/flutter/lib/plugin/utils/dialogs.dart b/flutter/lib/plugin/utils/dialogs.dart index c01dc368fad..6fdb86ab41c 100644 --- a/flutter/lib/plugin/utils/dialogs.dart +++ b/flutter/lib/plugin/utils/dialogs.dart @@ -2,14 +2,15 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import 'package:flutter_hbb/models/platform_model.dart'; void showPeerSelectionDialog( {bool singleSelection = false, required Function(List) onPeersCallback}) async { - final peers = await bind.mainGetRecentPeers(getAll: true); + // load recent peers, we can directly use the peers in `gFFI.recentPeersModel`. + // The plugin is not used for now, so just left it empty here. + final peers = ''; if (peers.isEmpty) { - debugPrint("load recent peers failed."); + // debugPrint("load recent peers failed."); return; } diff --git a/libs/hbb_common b/libs/hbb_common index f94753bddb1..16900b9b064 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit f94753bddb11a6de52e4fe7f9e490fcf8df2275a +Subproject commit 16900b9b064067e28f6e685b29a94c16350ffc36 diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 1a65658e221..fed038233a0 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1127,84 +1127,48 @@ pub fn main_load_recent_peers() { return; } - let push_to_flutter = |peers| { - let data = HashMap::from([("name", "load_recent_peers".to_owned()), ("peers", peers)]); + let push_to_flutter = |peers, ids| { + let mut data = + HashMap::from([("name", "load_recent_peers".to_owned()), ("peers", peers)]); + if let Some(ids) = ids { + data.insert("ids", ids); + } let _res = flutter::push_global_event( flutter::APP_TYPE_MAIN, serde_json::ser::to_string(&data).unwrap_or("".to_owned()), ); }; - let load_two_times = - vec_id_modified_time_path.len() > PeerConfig::BATCH_LOADING_COUNT && cfg!(target_os = "windows"); + let load_two_times = vec_id_modified_time_path.len() > PeerConfig::BATCH_LOADING_COUNT + && cfg!(target_os = "windows"); let mut all_peers = vec![]; if load_two_times { let next_from = load_recent_peers(&vec_id_modified_time_path, false, &mut all_peers, 0); - push_to_flutter(serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned())); + let rest_ids = if next_from < vec_id_modified_time_path.len() { + Some( + vec_id_modified_time_path[next_from..] + .iter() + .map(|(id, _, _)| id.clone()) + .collect::>() + .join(", "), + ) + } else { + None + }; + push_to_flutter( + serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned()), + rest_ids, + ); let _ = load_recent_peers(&vec_id_modified_time_path, true, &mut all_peers, next_from); } else { let _ = load_recent_peers(&vec_id_modified_time_path, true, &mut all_peers, 0); } - push_to_flutter(serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned())); - } -} - -fn get_partial_recent_peers( - vec_id_modified_time_path: Vec<(String, SystemTime, std::path::PathBuf)>, - count: usize, -) -> String { - let (peers, next_from) = PeerConfig::batch_peers( - &vec_id_modified_time_path, - 0, - Some(count.min(vec_id_modified_time_path.len())), - ); - let peer_maps: Vec<_> = peers - .into_iter() - .map(|(id, _, p)| peer_to_map(id, p)) - .collect(); - let mut data = HashMap::from([( - "peers", - serde_json::ser::to_string(&peer_maps).unwrap_or("[]".to_owned()), - )]); - if next_from < vec_id_modified_time_path.len() { - let ids: Vec<_> = vec_id_modified_time_path[next_from..] - .iter() - .map(|(id, _, _)| id.clone()) - .collect(); - data.insert( - "ids", - serde_json::ser::to_string(&ids).unwrap_or("[]".to_owned()), + // Don't check if `all_peers` is empty, because we need this message to update the state in the flutter side. + push_to_flutter( + serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned()), + None, ); } - return serde_json::ser::to_string(&data).unwrap_or("".to_owned()); -} - -pub fn main_get_recent_peers(get_all: bool) -> String { - if !config::APP_DIR.read().unwrap().is_empty() { - let vec_id_modified_time_path = PeerConfig::get_vec_id_modified_time_path(&None); - - let load_two_times = !get_all - && vec_id_modified_time_path.len() > PeerConfig::BATCH_LOADING_COUNT - && cfg!(target_os = "windows"); - let load_count = if load_two_times { - PeerConfig::BATCH_LOADING_COUNT - } else { - vec_id_modified_time_path.len() - }; - return get_partial_recent_peers(vec_id_modified_time_path, load_count); - } - "".to_string() -} - -pub fn main_load_lan_peers_sync() -> SyncReturn { - let data = HashMap::from([ - ("name", "load_lan_peers".to_owned()), - ( - "peers", - serde_json::to_string(&get_lan_peers()).unwrap_or_default(), - ), - ]); - return SyncReturn(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); } pub fn main_load_recent_peers_for_ab(filter: String) -> String { From f631c1c28d918c1920e825914eab5714f05e18e4 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 20 Feb 2025 19:35:04 +0800 Subject: [PATCH 103/506] refact: Remote ID editor, only select text on focus (#10854) Signed-off-by: fufesou --- flutter/lib/common/widgets/autocomplete.dart | 15 ++++++++----- .../lib/desktop/pages/connection_page.dart | 22 ++++++++----------- flutter/lib/mobile/pages/connection_page.dart | 19 ++++++++-------- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/flutter/lib/common/widgets/autocomplete.dart b/flutter/lib/common/widgets/autocomplete.dart index 1ea6d47be5d..ec64cca1892 100644 --- a/flutter/lib/common/widgets/autocomplete.dart +++ b/flutter/lib/common/widgets/autocomplete.dart @@ -8,13 +8,16 @@ import 'package:flutter_hbb/common/widgets/peer_card.dart'; class AllPeersLoader { List peers = []; - bool isPeersLoading = false; - bool isPeersLoaded = false; + bool _isPeersLoading = false; + bool _isPeersLoaded = false; final String _listenerKey = 'AllPeersLoader'; late void Function(VoidCallback) setState; + bool get needLoad => !_isPeersLoaded && !_isPeersLoading; + bool get isPeersLoaded => _isPeersLoaded; + AllPeersLoader(); void init(void Function(VoidCallback) setState) { @@ -33,10 +36,10 @@ class AllPeersLoader { } Future getAllPeers() async { - if (isPeersLoaded || isPeersLoading) { + if (!needLoad) { return; } - isPeersLoading = true; + _isPeersLoading = true; if (gFFI.recentPeersModel.peers.isEmpty) { bind.mainLoadRecentPeers(); @@ -96,8 +99,8 @@ class AllPeersLoader { peers = parsedPeers; setState(() { - isPeersLoading = false; - isPeersLoaded = true; + _isPeersLoading = false; + _isPeersLoaded = true; }); } } diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 2dc387067d1..0f83bfb1fb1 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -282,8 +282,15 @@ class _ConnectionPageState extends State void onFocusChanged() { _idInputFocused.value = _idFocusNode.hasFocus; - if (_idFocusNode.hasFocus && !_allPeersLoader.isPeersLoading) { - _allPeersLoader.getAllPeers(); + if (_idFocusNode.hasFocus) { + if (_allPeersLoader.needLoad) { + _allPeersLoader.getAllPeers(); + } + + final textLength = _idEditingController.value.text.length; + // Select all to facilitate removing text, just following the behavior of address input of chrome. + _idEditingController.selection = + TextSelection(baseOffset: 0, extentOffset: textLength); } } @@ -390,17 +397,6 @@ class _ConnectionPageState extends State ) { fieldTextEditingController.text = _idController.text; Get.put(fieldTextEditingController); - - // The listener will be added multiple times when the widget is rebuilt. - // We may need to use the `RawAutocomplete` to get the focus node. - - // Temporarily remove Selection because Selection can cause users to accidentally delete previously entered content during input. - // final textLength = - // fieldTextEditingController.value.text.length; - // // Select all to facilitate removing text, just following the behavior of address input of chrome. - // fieldTextEditingController.selection = - // TextSelection(baseOffset: 0, extentOffset: textLength); - return Obx(() => TextField( autocorrect: false, enableSuggestions: false, diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index e550200bfa1..1e8f4528ea6 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -104,8 +104,15 @@ class _ConnectionPageState extends State { void onFocusChanged() { _idEmpty.value = _idEditingController.text.isEmpty; - if (_idFocusNode.hasFocus && !_allPeersLoader.isPeersLoading) { - _allPeersLoader.getAllPeers(); + if (_idFocusNode.hasFocus) { + if (_allPeersLoader.needLoad) { + _allPeersLoader.getAllPeers(); + } + + final textLength = _idEditingController.value.text.length; + // Select all to facilitate removing text, just following the behavior of address input of chrome. + _idEditingController.selection = + TextSelection(baseOffset: 0, extentOffset: textLength); } } @@ -210,14 +217,6 @@ class _ConnectionPageState extends State { fieldTextEditingController.text = _idController.text; Get.put( fieldTextEditingController); - - // Temporarily remove Selection because Selection can cause users to accidentally delete previously entered content during input. - // final textLength = - // fieldTextEditingController.value.text.length; - // // select all to facilitate removing text, just following the behavior of address input of chrome - // fieldTextEditingController.selection = TextSelection( - // baseOffset: 0, extentOffset: textLength); - return AutoSizeTextField( controller: fieldTextEditingController, focusNode: fieldFocusNode, From ce1e4863cbf27c683918393fb46064dda941bbdd Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:44:18 +0800 Subject: [PATCH 104/506] fix: load peers, always push event data (#10856) Signed-off-by: fufesou --- src/flutter_ffi.rs | 47 +++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index fed038233a0..a43ac8f984a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1121,24 +1121,24 @@ fn load_recent_peers( } pub fn main_load_recent_peers() { + let push_to_flutter = |peers, ids| { + let mut data = HashMap::from([("name", "load_recent_peers".to_owned()), ("peers", peers)]); + if let Some(ids) = ids { + data.insert("ids", ids); + } + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + }; + if !config::APP_DIR.read().unwrap().is_empty() { let vec_id_modified_time_path = PeerConfig::get_vec_id_modified_time_path(&None); if vec_id_modified_time_path.is_empty() { + push_to_flutter("".to_owned(), None); return; } - let push_to_flutter = |peers, ids| { - let mut data = - HashMap::from([("name", "load_recent_peers".to_owned()), ("peers", peers)]); - if let Some(ids) = ids { - data.insert("ids", ids); - } - let _res = flutter::push_global_event( - flutter::APP_TYPE_MAIN, - serde_json::ser::to_string(&data).unwrap_or("".to_owned()), - ); - }; - let load_two_times = vec_id_modified_time_path.len() > PeerConfig::BATCH_LOADING_COUNT && cfg!(target_os = "windows"); let mut all_peers = vec![]; @@ -1168,6 +1168,8 @@ pub fn main_load_recent_peers() { serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned()), None, ); + } else { + push_to_flutter("".to_owned(), None) } } @@ -1189,6 +1191,13 @@ pub fn main_load_recent_peers_for_ab(filter: String) -> String { } pub fn main_load_fav_peers() { + let push_to_flutter = |peers| { + let data = HashMap::from([("name", "load_fav_peers".to_owned()), ("peers", peers)]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + }; if !config::APP_DIR.read().unwrap().is_empty() { let favs = get_fav(); let mut recent = PeerConfig::peers(Some(favs.clone())); @@ -1217,17 +1226,9 @@ pub fn main_load_fav_peers() { .map(|(id, _, p)| peer_to_map(id, p)) .collect(); - let data = HashMap::from([ - ("name", "load_fav_peers".to_owned()), - ( - "peers", - serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), - ), - ]); - let _res = flutter::push_global_event( - flutter::APP_TYPE_MAIN, - serde_json::ser::to_string(&data).unwrap_or("".to_owned()), - ); + push_to_flutter(serde_json::ser::to_string(&peers).unwrap_or("".to_owned())); + } else { + push_to_flutter("".to_owned()); } } From 343f12b3807447a764a23f2ef0d90bec0b3f43f2 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:47:42 +0800 Subject: [PATCH 105/506] fix: load local peers, called two times on select tab (#10859) Signed-off-by: fufesou --- flutter/lib/common/widgets/peer_tab_page.dart | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 53c9dce06dd..4849f278327 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -33,8 +33,8 @@ class PeerTabPage extends StatefulWidget { class _TabEntry { final Widget widget; - final Function({dynamic hint}) load; - _TabEntry(this.widget, this.load); + final Function({dynamic hint})? load; + _TabEntry(this.widget, [this.load]); } EdgeInsets? _menuPadding() { @@ -44,21 +44,15 @@ EdgeInsets? _menuPadding() { class _PeerTabPageState extends State with SingleTickerProviderStateMixin { final List<_TabEntry> entries = [ - _TabEntry( - RecentPeersView( - menuPadding: _menuPadding(), - ), - bind.mainLoadRecentPeers), - _TabEntry( - FavoritePeersView( - menuPadding: _menuPadding(), - ), - bind.mainLoadFavPeers), - _TabEntry( - DiscoveredPeersView( - menuPadding: _menuPadding(), - ), - bind.mainDiscover), + _TabEntry(RecentPeersView( + menuPadding: _menuPadding(), + )), + _TabEntry(FavoritePeersView( + menuPadding: _menuPadding(), + )), + _TabEntry(DiscoveredPeersView( + menuPadding: _menuPadding(), + )), _TabEntry( AddressBook( menuPadding: _menuPadding(), @@ -100,7 +94,7 @@ class _PeerTabPageState extends State gFFI.peerTabModel.setCurrentTabCachedPeers([]); } gFFI.peerTabModel.setCurrentTab(tabIndex); - entries[tabIndex].load(hint: false); + entries[tabIndex].load?.call(hint: false); } } @@ -225,7 +219,7 @@ class _PeerTabPageState extends State child: RefreshWidget( onPressed: () { if (gFFI.peerTabModel.currentTab < entries.length) { - entries[gFFI.peerTabModel.currentTab].load(); + entries[gFFI.peerTabModel.currentTab].load?.call(); } }, spinning: loading, From 0b9a6a280e6f49ace0de8e5bfec2947dcfe00411 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:41:57 +0800 Subject: [PATCH 106/506] fix: remote id, update text and reserve selection (#10867) Signed-off-by: fufesou --- flutter/lib/common.dart | 15 +++++++++++++++ flutter/lib/desktop/pages/connection_page.dart | 5 +++-- flutter/lib/mobile/pages/connection_page.dart | 6 +++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 2d4084851b5..777924c668b 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3705,3 +3705,18 @@ Widget workaroundWindowBorder(BuildContext context, Widget child) { ], ); } + +void updateTextAndPreserveSelection(TextEditingController controller, String text) { + final preSelectionStart = controller.selection.start; + final preSelectionEnd = controller.selection.end; + // Only care about select all for now. + final isSelected = preSelectionEnd > preSelectionStart; + + // Set text will make the selection invalid. + controller.text = text; + + if (isSelected) { + controller.selection = TextSelection( + baseOffset: 0, extentOffset: controller.value.text.length); + } +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 0f83bfb1fb1..dd9e840411f 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -225,6 +225,7 @@ class _ConnectionPageState extends State } }); } + Get.put(_idEditingController); Get.put(_idController); windowManager.addListener(this); } @@ -395,8 +396,8 @@ class _ConnectionPageState extends State FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { - fieldTextEditingController.text = _idController.text; - Get.put(fieldTextEditingController); + updateTextAndPreserveSelection( + fieldTextEditingController, _idController.text); return Obx(() => TextField( autocorrect: false, enableSuggestions: false, diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 1e8f4528ea6..07aaaef8cdf 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -74,6 +74,7 @@ class _ConnectionPageState extends State { } }); } + Get.put(_idEditingController); } @override @@ -214,9 +215,8 @@ class _ConnectionPageState extends State { TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted) { - fieldTextEditingController.text = _idController.text; - Get.put( - fieldTextEditingController); + updateTextAndPreserveSelection( + fieldTextEditingController, _idController.text); return AutoSizeTextField( controller: fieldTextEditingController, focusNode: fieldFocusNode, From 2575e14811dd84476372d2a2565e7edf8bd0a763 Mon Sep 17 00:00:00 2001 From: Theofanis Sarmidis <126983335+tsarmis@users.noreply.github.com> Date: Fri, 21 Feb 2025 04:42:12 +0200 Subject: [PATCH 107/506] Update el.rs (#10866) --- src/lang/el.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/el.rs b/src/lang/el.rs index 0e7038d293a..5f8dae0b129 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -41,7 +41,7 @@ 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, - (dash) και _ (υπογράμμιση). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."), + ("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9, - (παύλα) και _ (κάτω παύλα). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."), ("Website", "Ιστότοπος"), ("About", "Πληροφορίες"), ("Slogan_tip", "Φτιαγμένο με πάθος - σε έναν κόσμο που βυθίζεται στο χάος!"), @@ -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", "Διακοπή εγγραφής συνεδρίας"), @@ -630,7 +630,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("enable-bot-desc", "1, Ανοίξτε μια συνομιλία με τον @BotFather., Στείλτε την εντολή \"/newbot\". Θα λάβετε ένα διακριτικό αφού ολοκληρώσετε αυτό το βήμα.3, Ξεκινήστε μια συνομιλία με το bot που μόλις δημιουργήσατε. Στείλτε ένα μήνυμα που αρχίζει με κάθετο (\"/\") όπως \"/hello\" για να το ενεργοποιήσετε."), ("cancel-2fa-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το 2FA;"), ("cancel-bot-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το Telegram bot;"), - ("About RustDesk", ""), + ("About RustDesk", "Πληροφορίες για το RustDesk"), ("Send clipboard keystrokes", "Αποστολή προχείρου με πλήκτρα συντόμευσης"), ("network_error_tip", "Ελέγξτε τη σύνδεσή σας στο δίκτυο και, στη συνέχεια, κάντε κλικ στην επανάληψη."), ("Unlock with PIN", "Ξεκλείδωμα με PIN"), @@ -656,6 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Ενημέρωση απομακρισμένου προχείρου"), ("Untagged", "Χωρίς ετικέτα"), ("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"), - ("Accessible devices", ""), + ("Accessible devices", "Προσβάσιμες συσκευές"), ].iter().cloned().collect(); } From fc396d2166154252366f2c22bdfa755f05c4bf8b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:00:19 +0800 Subject: [PATCH 108/506] fix: check text editing controlling, if selection is valid (#10868) Signed-off-by: fufesou --- flutter/lib/common.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 777924c668b..205f30a683a 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3706,11 +3706,11 @@ Widget workaroundWindowBorder(BuildContext context, Widget child) { ); } -void updateTextAndPreserveSelection(TextEditingController controller, String text) { - final preSelectionStart = controller.selection.start; - final preSelectionEnd = controller.selection.end; +void updateTextAndPreserveSelection( + TextEditingController controller, String text) { // Only care about select all for now. - final isSelected = preSelectionEnd > preSelectionStart; + final isSelected = controller.selection.isValid && + controller.selection.end > controller.selection.start; // Set text will make the selection invalid. controller.text = text; From e191d11f7421d5833e3d83815c6b84be585abccb Mon Sep 17 00:00:00 2001 From: princeyogesh Date: Sat, 22 Feb 2025 10:47:11 +0530 Subject: [PATCH 109/506] before docker run command added git submoulde update command in docker build Updated README.md (#10878) there is dependency on submodule libs/hbb_common, we need to initialize submodule after cloning repo and before running container --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3e468e0909f..912c73a7df6 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ Begin by cloning the repository and building the Docker container: ```sh git clone https://github.com/rustdesk/rustdesk cd rustdesk +git submodule update --init --recursive docker build -t "rustdesk-builder" . ``` From 6f1a76974179d80a6b14934b053f2a1c79f8f8c7 Mon Sep 17 00:00:00 2001 From: luzpaz Date: Sat, 22 Feb 2025 20:01:52 -0500 Subject: [PATCH 110/506] fix: source typo in flutter/lib/common/widgets/address_book.dart (#10884) Found via codespell --- flutter/lib/common/widgets/address_book.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index deed97bb30b..6a3cec8ade8 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -509,13 +509,13 @@ class _AddressBookState extends State { double marginBottom = 4; - row({required Widget lable, required Widget input}) { + row({required Widget label, required Widget input}) { makeChild(bool isPortrait) => Row( children: [ !isPortrait ? ConstrainedBox( constraints: const BoxConstraints(minWidth: 100), - child: lable.marginOnly(right: 10)) + child: label.marginOnly(right: 10)) : SizedBox.shrink(), Expanded( child: ConstrainedBox( @@ -535,7 +535,7 @@ class _AddressBookState extends State { Column( children: [ row( - lable: Row( + label: Row( children: [ Text( '*', @@ -558,7 +558,7 @@ class _AddressBookState extends State { errorMaxLines: 5), ).workaroundFreezeLinuxMint())), row( - lable: Text( + label: Text( translate('Alias'), style: style, ), @@ -573,7 +573,7 @@ class _AddressBookState extends State { ), if (isCurrentAbShared) row( - lable: Text( + label: Text( translate('Password'), style: style, ), From 93feedc212a326587e4b61d36e8b02c1d69f171f Mon Sep 17 00:00:00 2001 From: Kleofass <4000163+Kleofass@users.noreply.github.com> Date: Sun, 23 Feb 2025 07:39:43 +0200 Subject: [PATCH 111/506] Update lv.rs (#10883) --- 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 81230102d3f..b15eafa7602 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "garums %min% līdz %max%"), ("starts with a letter", "sākas ar burtu"), ("allowed characters", "atļautās rakstzīmes"), - ("id_change_tip", "Atļautas tikai rakstzīmes a-z, A-Z, 0-9, - (dash) un _ (pasvītrojums). Pirmajam burtam ir jābūt a-z, A-Z. Garums no 6 līdz 16."), + ("id_change_tip", "Atļautas tikai rakstzīmes a-z, A-Z, 0-9, - (domuzīme) un _ (pasvītrojums). Pirmajam burtam ir jābūt a-z, A-Z. Garums no 6 līdz 16."), ("Website", "Tīmekļa vietne"), ("About", "Par"), ("Slogan_tip", "Radīts ar sirdi šajā haotiskajā pasaulē!"), @@ -656,6 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Atjaunināt klienta starpliktuvi"), ("Untagged", "Neatzīmēts"), ("new-version-of-{}-tip", "Ir pieejama jauna {} versija"), - ("Accessible devices", ""), + ("Accessible devices", "Pieejamas ierīces"), ].iter().cloned().collect(); } From c46023bbde31c2fae02c9608603bbb3ffe4de2c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 08:58:25 +0800 Subject: [PATCH 112/506] Git submodule: Bump libs/hbb_common from `16900b9` to `7cf11f7` (#10895) Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `16900b9` to `7cf11f7`. - [Commits](https://github.com/rustdesk/hbb_common/compare/16900b9b064067e28f6e685b29a94c16350ffc36...7cf11f7b771e27ecbd14fd1dd0ced55a64f40eb5) --- updated-dependencies: - dependency-name: libs/hbb_common dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index 16900b9b064..7cf11f7b771 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 16900b9b064067e28f6e685b29a94c16350ffc36 +Subproject commit 7cf11f7b771e27ecbd14fd1dd0ced55a64f40eb5 From 3a5b30a5e7064bd0b715733689884b3e8de96e83 Mon Sep 17 00:00:00 2001 From: Andrzej Rudnik Date: Mon, 24 Feb 2025 16:08:48 +0100 Subject: [PATCH 113/506] Update pl.rs (#10901) --- src/lang/pl.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index a8848bc9554..3d4dd3ac797 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -654,8 +654,8 @@ 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", ""), - ("new-version-of-{}-tip", ""), - ("Accessible devices", ""), + ("Untagged", "Bez etykiety"), + ("new-version-of-{}-tip", "Dostępna jest nowa wersja {}"), + ("Accessible devices", "Dostępne urządzenia"), ].iter().cloned().collect(); } From 280c12942f9b10d167c821d54853b02fbc07c8b0 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 25 Feb 2025 00:30:29 +0800 Subject: [PATCH 114/506] improve android build --- flutter/android/gradle.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/android/gradle.properties b/flutter/android/gradle.properties index 94adc3a3f97..804b29b300a 100644 --- a/flutter/android/gradle.properties +++ b/flutter/android/gradle.properties @@ -1,3 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx1024M android.useAndroidX=true android.enableJetifier=true +org.gradle.daemon=false From d8496aba0b2ef654d84df89ccdf94ea84efa27f6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 26 Feb 2025 00:32:54 +0800 Subject: [PATCH 115/506] refactor is_custom_client --- src/common.rs | 8 ++++++++ src/flutter_ffi.rs | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index 9865ab85c13..dfd3fca44ac 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1721,9 +1721,17 @@ pub fn get_builtin_option(key: &str) -> String { .unwrap_or_default() } +#[inline] +pub fn is_custom_client() -> bool { + get_app_name() != "RustDesk" +} + pub fn verify_login(raw: &str, id: &str) -> bool { true /* + if is_custom_client() { + return true; + } #[cfg(debug_assertions)] return true; let Ok(pk) = crate::decode64("IycjQd4TmWvjjLnYd796Rd+XkK+KG+7GU1Ia7u4+vSw=") else { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a43ac8f984a..cc9d304e7c5 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -2015,7 +2015,7 @@ pub fn is_outgoing_only() -> SyncReturn { } pub fn is_custom_client() -> SyncReturn { - SyncReturn(get_app_name() != "RustDesk") + SyncReturn(crate::common::is_custom_client()) } pub fn is_disable_settings() -> SyncReturn { From bc3a58f6f4882d3ace0e7a9fad26472f257c7480 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 26 Feb 2025 18:00:31 +0800 Subject: [PATCH 116/506] 1.3.9 --- .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 1bdcd1401c3..825c67ec433 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: 2025.01.13 VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" - VERSION: "1.3.8" + VERSION: "1.3.9" 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 b98cc9618b5..97ea99085c4 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.8" + VERSION: "1.3.9" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/Cargo.lock b/Cargo.lock index 12057fc8fbc..e67a1b58736 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5507,7 +5507,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.8" +version = "1.3.9" dependencies = [ "android-wakelock", "android_logger", @@ -5607,7 +5607,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.8" +version = "1.3.9" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index 034c790fce0..6de64f5e73b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.8" +version = "1.3.9" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index b49bd7ed9e9..8e0f2590679 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.8 + version: 1.3.9 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 51fe2584e6c..d117d17178d 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.8 + version: 1.3.9 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 0c389e2224a..62d29b1b13f 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.8+57 +version: 1.3.9+57 environment: sdk: '^3.1.0' diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index 132fbcc13a6..418e122f734 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.8" +version = "1.3.9" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index 775108c8f27..b5ed113700a 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.8 +pkgver=1.3.9 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index add593685f6..421bcb25a8c 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.8 +Version: 1.3.9 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index b30d0f9de65..508b8050fcd 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.8 +Version: 1.3.9 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index 502340946c7..ebfefb8a080 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.8 +Version: 1.3.9 Release: 0 Summary: RPM package License: GPL-3.0 From 00293a9902d80f8159fecf46556cfd3db7fc87af Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 28 Feb 2025 00:46:46 +0800 Subject: [PATCH 117/506] Feat/macos clipboard file (#10939) * feat: macos, clipboard file Signed-off-by: fufesou * Can't reuse file transfer Signed-off-by: fufesou * handle paste task Signed-off-by: fufesou --------- Signed-off-by: fufesou --- Cargo.lock | 37 + libs/clipboard/Cargo.toml | 8 + libs/clipboard/src/context_send.rs | 23 +- libs/clipboard/src/lib.rs | 36 +- libs/clipboard/src/platform/mod.rs | 10 + libs/clipboard/src/platform/unix/filetype.rs | 10 +- .../src/platform/unix/macos/README.md | 25 + .../platform/unix/macos/item_data_provider.rs | 77 +++ libs/clipboard/src/platform/unix/macos/mod.rs | 14 + .../platform/unix/macos/paste-files-macos.png | Bin 0 -> 39355 bytes .../src/platform/unix/macos/paste_observer.rs | 179 +++++ .../src/platform/unix/macos/paste_task.rs | 639 ++++++++++++++++++ .../platform/unix/macos/pasteboard_context.rs | 443 ++++++++++++ libs/clipboard/src/platform/unix/mod.rs | 4 + libs/clipboard/src/platform/windows.rs | 17 +- src/client.rs | 4 + src/client/io_loop.rs | 50 +- src/clipboard.rs | 86 ++- src/common.rs | 4 + src/server/clipboard_service.rs | 4 + src/server/connection.rs | 45 +- 21 files changed, 1654 insertions(+), 61 deletions(-) create mode 100644 libs/clipboard/src/platform/unix/macos/README.md create mode 100644 libs/clipboard/src/platform/unix/macos/item_data_provider.rs create mode 100644 libs/clipboard/src/platform/unix/macos/mod.rs create mode 100644 libs/clipboard/src/platform/unix/macos/paste-files-macos.png create mode 100644 libs/clipboard/src/platform/unix/macos/paste_observer.rs create mode 100644 libs/clipboard/src/platform/unix/macos/paste_task.rs create mode 100644 libs/clipboard/src/platform/unix/macos/pasteboard_context.rs diff --git a/Cargo.lock b/Cargo.lock index e67a1b58736..2652296ec1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -978,10 +978,15 @@ dependencies = [ "cacao", "cc", "dashmap", + "dirs 5.0.1", + "fsevent", "fuser", "hbb_common", "lazy_static", "libc", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", "once_cell", "parking_lot", "percent-encoding", @@ -990,8 +995,10 @@ dependencies = [ "serde_derive", "thiserror", "utf16string", + "uuid", "x11-clipboard 0.8.1", "x11rb 0.12.0", + "xattr", ] [[package]] @@ -2218,6 +2225,25 @@ dependencies = [ "time 0.1.45", ] +[[package]] +name = "fsevent" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8836d1f147a0a195bf517a5fd211ea7023d19ced903135faf6c4504f2cf8775f" +dependencies = [ + "bitflags 1.3.2", + "fsevent-sys", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -7999,6 +8025,17 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "xattr" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" +dependencies = [ + "libc", + "linux-raw-sys 0.4.14", + "rustix 0.38.34", +] + [[package]] name = "xdg-home" version = "1.2.0" diff --git a/libs/clipboard/Cargo.toml b/libs/clipboard/Cargo.toml index 9db2e5a9954..afe2f2f3137 100644 --- a/libs/clipboard/Cargo.toml +++ b/libs/clipboard/Cargo.toml @@ -47,3 +47,11 @@ fuser = {version = "0.15", default-features = false, optional = true} [target.'cfg(target_os = "macos")'.dependencies] cacao = {git="https://github.com/clslaid/cacao", branch = "feat/set-file-urls", optional = true} +# Use `relax-void-encoding`, as that allows us to pass `c_void` instead of implementing `Encode` correctly for `&CGImageRef` +objc2 = { version = "0.5.1", features = ["relax-void-encoding"] } +objc2-foundation = { version = "0.2.0", features = ["NSArray", "NSString", "NSEnumerator", "NSGeometry", "NSProgress"] } +objc2-app-kit = { version = "0.2.0", features = ["NSPasteboard", "NSPasteboardItem", "NSImage", "NSFilePromiseProvider"] } +uuid = { version = "1.3", features = ["v4"] } +fsevent = "2.1.2" +dirs = "5.0" +xattr = "1.4.0" diff --git a/libs/clipboard/src/context_send.rs b/libs/clipboard/src/context_send.rs index f3606509f01..caa9d4a4883 100644 --- a/libs/clipboard/src/context_send.rs +++ b/libs/clipboard/src/context_send.rs @@ -1,22 +1,29 @@ use hbb_common::{log, ResultType}; -use std::sync::Mutex; +use std::{ops::Deref, sync::Mutex}; use crate::CliprdrServiceContext; const CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS: u32 = 30; lazy_static::lazy_static! { - static ref CONTEXT_SEND: ContextSend = ContextSend{addr: Mutex::new(None)}; + static ref CONTEXT_SEND: ContextSend = ContextSend::default(); } -pub struct ContextSend { - addr: Mutex>>, +#[derive(Default)] +pub struct ContextSend(Mutex>>); + +impl Deref for ContextSend { + type Target = Mutex>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } } impl ContextSend { #[inline] pub fn is_enabled() -> bool { - CONTEXT_SEND.addr.lock().unwrap().is_some() + CONTEXT_SEND.lock().unwrap().is_some() } pub fn set_is_stopped() { @@ -24,7 +31,7 @@ impl ContextSend { } pub fn enable(enabled: bool) { - let mut lock = CONTEXT_SEND.addr.lock().unwrap(); + let mut lock = CONTEXT_SEND.lock().unwrap(); if enabled { if lock.is_some() { return; @@ -49,7 +56,7 @@ impl ContextSend { /// make sure the clipboard context is enabled. pub fn make_sure_enabled() -> ResultType<()> { - let mut lock = CONTEXT_SEND.addr.lock().unwrap(); + let mut lock = CONTEXT_SEND.lock().unwrap(); if lock.is_some() { return Ok(()); } @@ -63,7 +70,7 @@ impl ContextSend { pub fn proc) -> ResultType<()>>( f: F, ) -> ResultType<()> { - let mut lock = CONTEXT_SEND.addr.lock().unwrap(); + let mut lock = CONTEXT_SEND.lock().unwrap(); match lock.as_mut() { Some(context) => f(context), None => Ok(()), diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index 57e6ce6175f..f28fe083da8 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -1,6 +1,9 @@ use std::sync::{Arc, Mutex, RwLock}; -#[cfg(target_os = "windows")] +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] use hbb_common::ResultType; #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] use hbb_common::{allow_err, log}; @@ -14,10 +17,16 @@ use hbb_common::{ use serde_derive::{Deserialize, Serialize}; use thiserror::Error; -#[cfg(target_os = "windows")] +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] pub mod context_send; pub mod platform; -#[cfg(target_os = "windows")] +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] pub use context_send::*; #[cfg(target_os = "windows")] @@ -27,9 +36,18 @@ const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002; #[cfg(target_os = "windows")] const ERR_CODE_SEND_MSG: u32 = 0x00000003; -#[cfg(target_os = "windows")] +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] pub(crate) use platform::create_cliprdr_context; +pub struct ProgressPercent { + pub percent: f64, + pub is_canceled: bool, + pub is_failed: bool, +} + // to-do: This trait may be removed, because unix file copy paste does not need it. /// Ability to handle Clipboard File from remote rustdesk client /// @@ -44,6 +62,10 @@ pub trait CliprdrServiceContext: Send + Sync { fn empty_clipboard(&mut self, conn_id: i32) -> Result; /// run as a server for clipboard RPC fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError>; + /// get the progress of the paste task. + fn get_progress_percent(&self) -> Option; + /// cancel the paste task. + fn cancel(&mut self); } #[derive(Error, Debug)] @@ -62,11 +84,11 @@ pub enum CliprdrError { ConversionFailure, #[error("failure to read clipboard")] OpenClipboard, - #[error("failure to read file metadata or content")] + #[error("failure to read file metadata or content, path: {path}, err: {err}")] FileError { path: String, err: std::io::Error }, - #[error("invalid request")] + #[error("invalid request: {description}")] InvalidRequest { description: String }, - #[error("common request")] + #[error("common request: {description}")] CommonError { description: String }, #[error("unknown cliprdr error")] Unknown(u32), diff --git a/libs/clipboard/src/platform/mod.rs b/libs/clipboard/src/platform/mod.rs index 5bf1279cbad..f7d28f32267 100644 --- a/libs/clipboard/src/platform/mod.rs +++ b/libs/clipboard/src/platform/mod.rs @@ -14,3 +14,13 @@ pub fn create_cliprdr_context( #[cfg(feature = "unix-file-copy-paste")] pub mod unix; + +#[cfg(target_os = "macos")] +pub fn create_cliprdr_context( + _enable_files: bool, + _enable_others: bool, + _response_wait_timeout_secs: u32, +) -> crate::ResultType> { + let boxed = unix::macos::pasteboard_context::create_pasteboard_context()? as Box<_>; + Ok(boxed) +} diff --git a/libs/clipboard/src/platform/unix/filetype.rs b/libs/clipboard/src/platform/unix/filetype.rs index 6387a3ece3e..8436ba05ee2 100644 --- a/libs/clipboard/src/platform/unix/filetype.rs +++ b/libs/clipboard/src/platform/unix/filetype.rs @@ -4,15 +4,17 @@ use hbb_common::{ bytes::{Buf, Bytes}, log, }; +use serde_derive::{Deserialize, Serialize}; use std::{ path::PathBuf, time::{Duration, SystemTime}, }; use utf16string::WStr; +#[cfg(target_os = "linux")] pub type Inode = u64; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum FileType { File, Directory, @@ -28,10 +30,11 @@ pub const PERM_RW: u16 = 0o644; pub const PERM_SELF_RO: u16 = 0o400; /// rwx pub const PERM_RWX: u16 = 0o755; +#[allow(dead_code)] /// max length of file name pub const MAX_NAME_LEN: usize = 255; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct FileDescription { pub conn_id: i32, pub name: PathBuf, @@ -40,9 +43,7 @@ pub struct FileDescription { pub last_modified: SystemTime, pub last_metadata_changed: SystemTime, pub creation_time: SystemTime, - pub size: u64, - pub perm: u16, } @@ -144,7 +145,6 @@ impl FileDescription { atime: last_modified, last_modified, last_metadata_changed: last_modified, - creation_time: last_modified, size, perm, diff --git a/libs/clipboard/src/platform/unix/macos/README.md b/libs/clipboard/src/platform/unix/macos/README.md new file mode 100644 index 00000000000..5c1cc5c909e --- /dev/null +++ b/libs/clipboard/src/platform/unix/macos/README.md @@ -0,0 +1,25 @@ +# File pate on macOS + +MacOS cannot use `fuse` because of [macfuse is not supported by default](https://github.com/macfuse/macfuse/wiki/Getting-Started#enabling-support-for-third-party-kernel-extensions-apple-silicon-macs). + +1. Use a temporary file `/tmp/rustdesk_` as a placeholder in the pasteboard. +2. Uses `fsevent` to observe files paste operation. Then perform pasting files. + +## Files + +### `pasteboard_context.rs` + +The context manager of the paste operations. + +### `item_data_provider.rs` + +1. Set pasteboard item. +2. Create temp file in `/tmp/.rustdesk_*`. + +### `paste_observer.rs` + +Use `fsevent` to observe the paste operation with the source file `/tmp/.rustdesk_*`. + +### `paste_task.rs` + +Perform the paste. diff --git a/libs/clipboard/src/platform/unix/macos/item_data_provider.rs b/libs/clipboard/src/platform/unix/macos/item_data_provider.rs new file mode 100644 index 00000000000..b12e47d8013 --- /dev/null +++ b/libs/clipboard/src/platform/unix/macos/item_data_provider.rs @@ -0,0 +1,77 @@ +use super::pasteboard_context::{PasteObserverInfo, TEMP_FILE_PREFIX}; +use objc2::{ + declare_class, msg_send_id, mutability, + rc::Id, + runtime::{NSObject, NSObjectProtocol}, + ClassType, DeclaredClass, +}; +use objc2_app_kit::{ + NSPasteboard, NSPasteboardItem, NSPasteboardItemDataProvider, NSPasteboardType, + NSPasteboardTypeFileURL, +}; +use objc2_foundation::NSString; +use std::{io::Result, sync::mpsc::Sender}; + +pub(super) struct Ivars { + task_info: PasteObserverInfo, + tx: Sender>, +} + +declare_class!( + pub(super) struct PasteboardFileUrlProvider; + + unsafe impl ClassType for PasteboardFileUrlProvider { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + const NAME: &'static str = "PasteboardFileUrlProvider"; + } + + impl DeclaredClass for PasteboardFileUrlProvider { + type Ivars = Ivars; + } + + unsafe impl NSObjectProtocol for PasteboardFileUrlProvider {} + + unsafe impl NSPasteboardItemDataProvider for PasteboardFileUrlProvider { + #[method(pasteboard:item:provideDataForType:)] + #[allow(non_snake_case)] + unsafe fn pasteboard_item_provideDataForType( + &self, + _pasteboard: Option<&NSPasteboard>, + item: &NSPasteboardItem, + r#type: &NSPasteboardType, + ) { + if r#type == NSPasteboardTypeFileURL { + let path = format!("/tmp/{}{}", TEMP_FILE_PREFIX, uuid::Uuid::new_v4().to_string()); + match std::fs::File::create(&path) { + Ok(_) => { + let url = format!("file:///{}", &path); + item.setString_forType(&NSString::from_str(&url), &NSPasteboardTypeFileURL); + let mut task_info = self.ivars().task_info.clone(); + task_info.source_path = path; + self.ivars().tx.send(Ok(task_info)).ok(); + } + Err(e) => { + self.ivars().tx.send(Err(e)).ok(); + } + } + } + } + + // #[method(pasteboardFinishedWithDataProvider:)] + // unsafe fn pasteboardFinishedWithDataProvider(&self, pasteboard: &NSPasteboard) { + // } + } + + unsafe impl PasteboardFileUrlProvider {} +); + +pub(super) fn create_pasteboard_file_url_provider( + task_info: PasteObserverInfo, + tx: Sender>, +) -> Id { + let provider = PasteboardFileUrlProvider::alloc(); + let provider = provider.set_ivars(Ivars { task_info, tx }); + let provider: Id = unsafe { msg_send_id![super(provider), init] }; + provider +} diff --git a/libs/clipboard/src/platform/unix/macos/mod.rs b/libs/clipboard/src/platform/unix/macos/mod.rs new file mode 100644 index 00000000000..8b114aa17a8 --- /dev/null +++ b/libs/clipboard/src/platform/unix/macos/mod.rs @@ -0,0 +1,14 @@ +mod item_data_provider; +mod paste_observer; +mod paste_task; +pub mod pasteboard_context; + +pub fn should_handle_msg(msg: &crate::ClipboardFile) -> bool { + matches!( + msg, + crate::ClipboardFile::FormatList { .. } + | crate::ClipboardFile::FormatDataResponse { .. } + | crate::ClipboardFile::FileContentsResponse { .. } + | crate::ClipboardFile::TryEmpty + ) +} diff --git a/libs/clipboard/src/platform/unix/macos/paste-files-macos.png b/libs/clipboard/src/platform/unix/macos/paste-files-macos.png new file mode 100644 index 0000000000000000000000000000000000000000..73e4e3f0b696326515b4ad7acabde96e9f66600c GIT binary patch literal 39355 zcmcG0bwCqZ`|w1Os|a4N0t!q;MO3y=@^~T z4blz2vjJE1z2BeTA2089JLl>1)OpU_pGb<4ouobqfk4O}KDZ|hfgGEGK#tz~iwJyz zBoiM1|98YxTI>!awVrkY{NtGRZHe0u2*!tW>p3y__X(p1ilz_*II+YJb3d=gzzL-%h!|e6mGPOV{zF`SZyW7O$cAPkwSk zQ82!8x_9gH&Fd?C!RPO*)JJr<7f0n3$xiFPw75^xohJwtm&j-;3a$Pu31f|wkrGpB z=VBk03sU%C9^+Q~%@bW^dMoG_Ysc%p$j-$tc|`(R2BWM#!ajoir%(4jAJrNiy?6$_ z!fcw0*gOG@9an95eOtM8hNA?YQ_YCl$wFRUt9nU2Wc6LTaKY4d5`vLaS zGkCmCJhbHW@;)e(g{5Ud?`&7czyRIY0a1S`l=KV3;RWVXZ7VA)m6dMwZA|=1zVs6Z ztPuGYN+I&Ia1-OQ%nbH$YHtnE1KypUghxb72&8E1={*)r#^WRK`PcXQdK9WfC%<>r zh_59HFSLK~LOV7*Ir+#^H&(AbRnTDzk#=@pGENwuJ}ZZxu9x~9l%;N6vR}*F(fuIg zXvLYI_OftkzWnsEO3;N!T8Kkee}8+n(hGZgK`PAaJpqbH7&;~wF!CBW#8LfJXK%gO zr&5iSwKV(Lyx7evT)f@9XbVJ;bll~31qJ%ri?r0#rKp?xYS5*&oK9jDZ1Cc>8}B$R zY|TAcDJ9g5)6#@)PSS8oR<#v)c8sm6`PMmXJ&}UN4LdhtL_C)^x9jR8B2pBabH~QU zZ1R})g~6SGT3(u-N``iexS+PDbC@1ti4e|Z&y0+WE}z*P4Kt+PRhpidNKngiI-uN4 zpBqN$gu5Tz)YSChoPJunj48^;$Gi|#MQiSNX3v0}Lv_uk$4>XJt+kFzWa^s z;>U+ULh})u_b6|pkMHVw%0|U=D=EmO{32WzN$#Z9irnBBT&FqO0{-GJzq6zS2(yff8RK%wR2%f^H!wx z$AB0CdU;DrOA;4nbbyJ2L+TX^M@RLBTl;7DW5mYx4mc*?;^zm;Pbl8Mr6466 zwReD?_&xgg^*pb*Vygd}djr>k>F#)mCTdI0Mb#=5n>#!Ofr@4A*3jO*+SLCej~4`HL59{hqXv%Kl?m{HOdH|+mR zvGx2ImSLpXkcDEM41e+zx6mC{=QdN+G$>c;c*P>G+1=bg=sU=Gj&}^=q!-;I7CUQh zVsYovnDURaDnO z@j)%(MVt-0zHZz1gxZVQ3`1|_=U84yz;gP|!iO@oWfw$hZG&e!GE^ff3rA{?CTET) zt*Wh)5!=ZwzK|pZfdL;9KE~aICMDify4)(fk&!>K>L2L(&5K^%i^bl(t0zv&E4(&5 zSGazswXKIouUE7-T|-p8)%u0D-AZlk>2HN^Lr|>+p*t2>AxFf5!nQd!m9kxU zVy4;Qc%_vBQ$`nyj{uRqNT4!J6ggmT0?G*d^kQBr(}m_h*R#f&bnIg8_6~De;UZ1% zoz|zLtrSQt*>~+}DqBD1 zN=Z_aWz52?FLMpAfl7L(-b)?x90AP`od(Vy#hU8139qk08GTe&jzK2%MjTy02zeI! z^6|%u{N$NN?C~d2w4{$JAJ^8PAtKOb+gyX*=CsSqK*6PtnlPnYJglR#Wf3U*tu+i9 z;-qu!1&DH`QB4>T{>?qMe&XFOf(S;*rr25$ehbLf?jAnKccg#yv9M_B%RIRx6MQk$Y8dIx0@R8 zzL2B<*@O`A3D4O1h>j8s*ZcxlN_6UN`SP;4;E~G^2i0F>FqN*audl4+pdpPGeZPGU z0t{`_DRp)Yy7t!xnzhnCmauH{=;B^ykpJ*@n-vNGMY)FStCBG8vT|T_eMbl7Pqm zn=;=XCRfxl_A8vH6{*`@V`s3q`sHu$i;G0xR)cK+&fGl-G|5H0x6Gj{QckQyB|tzva2{BO5Bc=aRXb+WkCDZ6{gC;x z%s9h`4IZ&RDV*J*`nN)%%<}oCKlHd|<_67a6o%HGXZ_F<*oW@17Enw&I>?+*SG(|x zp}=3MAU$tsLFV4-S!Q%`iN^B0J;g-_hkgd$Z=R!QcPq5gwPnp~MHd!v!F_2h9#?V` zFk92L$Ct?RiW++2b*xi{FJd1>`mG%gp*4hMx@TZjwrTIHd-GCN>2vrNvzzPba$Xox z)%O7E=me%wN(!{`dJIZKbBQPkY0XVxA94Ckl^IuvcvV!V@&dMHV#Z@Br*iYXzDV7J zSC@;-t_i9Z`Ar&#YFTE${h0i^#Pin37d8VsZAM&}XMOXR^J{BsKWDOju$uV3Hrs58 zE@5SMJRLGI*o!r7hR2uG3)K!6U!T&V`fkWyl;FQ4-E>^Kz_-`w`F8cl<2iOM6?rL! z({)(}c1Ycd2gYg}sY7X3hw2h$q8NL!99y_boev3pj4LM()A_^n0n;qi=&xax-L9d;^{n!E9aO-KWLDcd$mJ; zy+$)gnqyV_OLQ%46#6vt@x|?|nK8}4G(@@GT@K#d5T-@%&b+3;jDip9blD_s5$?H^ zbsRl$0sYeM2lj9q*u#fbi|O_CJiGYekFVNRDHLd>la7viEj1@7GycqpOV|@N;VP0M}38~>6Va58ah}!K6 z((_*J?5{+IwXk8e-}Ot<9o83OR}mI>Iog-Fu0Jx6-8^H+S#mF{ck5(=;JXs8cLv;Z zFJra>9|1pI`_5C~Nb0}~Nlt_uHW8QJ-Q7LO+)p~!vF6#5qWN}l^1Z#u->L#JsRG|q zd){W4`{m73#lm&EG;zmkE-F2Bm>Fyn@0Pmxn-pCDg~GgQtmtY(hm3F?c*rJaf76@w zmD(PwE9R@X9Q8cAPQjClWK}9ST=0z|<3gL~&f{));@IZAq#cx7mGC}4_zbYf@UIo& zTA98m3CZ%w3I5bYf5A9O@^_?Sl_PH#JC!}&wgk{U$v*9Rf;mLt3%jjCC7i{8O)c4s zgmZy0NI%(5*)`0wMyxwIoAXNX?(|nb)x`CmWQ$Q3IH23=&)b(?SIlTcR^DuIdA_{` zbEW6trD{)8>$CImmDcqOJ*&Bh6&Jr+!S+@@0ln1@1PKjFRQo$2hnfZRvyN>BR?zA3mBVE!n*q?XAiO0X12DhDT1>a#(oev6?%9#_ z%J~MFI@Ea3!Li3dh7?Q;43f#O4dlu->4m5iQ)-^uSe83I7h|M-w_rQZB}f(qGOk}=-2z|XqG0mmg<5=neD%)>{Fj|uP{#h` znVDDoE1_d0a3D46Psk%5*#VPGw1AQTez<&uo1n08J1!Gs1ACBZ4Udq1s|c6Z8%3#p z*XcNxmbAC)0)E#?DpQj(eQ3dFyl5yzt^&uwRSsn9`s0Np>SY(k^AL@kfc#)T*|T7a zSTss?4Zwnuemn!z5jt>1s+4KVV+)9VI z@h^}Pkbw;6<4u;);V+XY)ssDE6M61fUQXHNmu8{pChO^{#fuRpvBb`T@6O7LIO zIf^IB!;^nKwgZF3V%rWOhlnYO5Q81jX4sX@?Ua<1?QQ$bxuavP6a+q7@W~tpz@`2W zTWy)%J)@?g@_9WFBt-K4c>hhiY@sg|AGzFyO1j(ub)Zc*( zGXHGf=~CYuq$>FW{Oo}k3E+X{=^k0u>4;npiCnL@ERNPrA>WtTz@NFt5Vvu(I^*12EdypVtLc5OXJeBpGCtreP^!y{-B!G(WrB&$_ zfa{G50Q#`v(9(av(#!azDyph-k-x9bLi?Wrph-?BSQmxb;LpomkI)nNoyHe{Er}S_ z*k_<6w`4|kK;;8KWeTvwu6q$~yc>#IOwo+&?nk%O;t(oLkP06{784!-~|4qlN$&;h^(EaAUo zhA)5QK_!(P2LB}+LRqm108jCoQ}fV48Q)6eLLlVh76rgC@%c0Yq@!JRWx#8aoW3|7 z%>Z#aIvjZln4%W|my8#xrG>@l4;=)=DHAvoPJlWce;WFpv32zI16ru%+d%VN{>I;kl1oz)&7YH`Ob(6GwB#cLNg#;3*sn*;rmJVBr^mIJ5$fvka_@mvk}K(AFG8W_1-+i0q=IA+Cknuo z2sX6`0IP3%t(F!~b*BR#M*>?g;cmo`0bFNkFq#b1n0OsldCy?idW@*?6tmfKCIeN z>Tx6Hvop$-WE?@2oR&7_;60yf@fA2pJ1$482{om`N2JSbY}9cAFRGWtFtF$A%JAF3 z*YAYJdFw`B%kpaE&Tz%Z)Ew=&=IrWhqKchyNV3&-L`e6(%LIvs?As6gmg`n-({&5&JaHi>(t7Iv2A!2R&j`d zUy-+qY@4?7V$r4EbN!-~8V-}Tq0&PMNjp0p)xGtM>U@^0?fq4Kq$+ptvQ8%T6{f4{ z?>gbJH3@8|zpfD|+V!k--PzU(ILth?1m~A#SZErLcw%gIFE^2%fi5Orv_bfte0xf> z?Y8~0d)>>?W#i`B_bfvvBCp z7WExuXbm6Hv((D9~jil0a!_S)2`(WDgap%#;hR{A#HS{VO8aBKONntDM%+VIy zyMo-aA_ECw8`~6`1+O&Jf_cqt-S#kl-V@Xj5KBTfpo}xOh--8ZU#|2E#D;v3qNXp( zr+ZU-^Rw0{FSmD>bFHGp^L(7w*3om>GS%s`T%+4hR}rVa$|5RjrA(d|+YsTTM75m1 z8njza)>=k%%T^hLg;2#I1qtUgnY1PH{YZ;SL4qkhJZVmvCEHEWtLNtAY-|^!iIY`| zt*bfi1_e8?j%$1iNmRN~l7O(g&Zh9B;#ATn4QxxqX#RW!u3&!iErV@vXHvC)XvQp8 zCBr9#?`Xv%OQv0~gjNRk&yuMqhDDmR#g^8!9S>hIZTXyZA%0p%rnWU^r^n+6D<_nB znGxTx;Y=0{K71xXlO)T5vzX8+J;NQQ-NQdR@B|x|mg-L(QgqAF3)dbIcdf6ME>xW< zEc&MIa?8R-_a~{yxPDnCT?Z*nDOK;ev>VoDS|g;|pz190pisfH3hLseSWD+v-$=D> zCV*=61{=%)4Z+Gw5h^FfPVTrobhtZjlOIVw9cXAQ|HEwNg*O zoV&Ors_Cq>;q&Ekwh;#mjNmH2%CJ~6QJJ0;!{EJwaEt!MPm+*0+nX!noomP9Fv^te z7&qvXRlX2K{ipU7y*+9n7m>Z@@eeF+nisvWFB_1_`uv1cMDW5k#YjPlVQ6uBb??0D z=b~HRI6*c9d@!!r$3Y%*f42Di*k>S) zPRy0{dzxl*yiv^GTmw&j5}R9T(V>uu=gLwrN&dJXCwL{R85$l5Gbh>e=BBz zvjN2E7+4@KU1a>$QHq~BAw4fx=7(GPr#E^VxO9+kezYeXH`jQ53t&sH z`Gkf0dwOc+7(vDM@)98g4cvs|!Ch|A>6n?B8BDN(Q9>Gp_CeOn0;-QySP@c2NY~qa zS`!F6ZtYcOBAbAHymXiokR$Oyp8U6E_eu+=l~2flBQQ#|k`>S)!wvI4*QX=%g3tcL zy_eJe0uw{{*!z#wb^REX+7s?^dnxI^e2q>Z*3SToeWJCOnL}m>evV0iTN#furfx5f z{+ISdoxP8(e`hJt0sryv1MBM34R<#DKxvkB8YuQl2gcGcIYrfR`vY1jUo1^sYL+#B zDx1b~XnoNmmqNMmc#86H=7;rEmSMO`CZ6u*<1S?&zxXy507=mipoM9`RU7^S%@366F=;eAG^1d`HJ*$RAbLqZskI zW>$4}2e-<6z1!39ebs!|Jo8XULUs$a>xHwO1yTley_f-=o7I9eAyrE&ONGq^-(QKj z2h}&|U+E>_dc4;#CD4(1LrZ$dQc62fbusVhP?aVhxL)-R&LmPSQ^qmV)+ON7;>;KgBpN(}YNjiL zLqn9X0?}qZ3%Q{dpY9mUS7}sOoZ<~&QpJ2w>T}%P7GBie@!Bc`*-T95Ug`NSH#~K> zbeS!2{^mpOD)3mU?^ywKS>jV+!z;I5)aPiVJ|`=$9h9yh3HMd+DLfsEr+kRjJB*`8 zw5lkuHgyJEc{`4zZBYmw={PJVAq#zh0Cq*2>ijs1N38Gj1kr4to7C2w&czi-)^R64 z4^~vU)g|)W-K!;K)c!_*vH(0JKYEFh%<`#88kuEWsa4U7V>JtVg*M^5|4c;=Z9MD^ zWh|&Rubc5#yKGvZGZ1QGOTli)9HNIPXUqH|o8_!=zc9O{ZoPs=D5__$GJB}tK^+g1 zECHkw$~D7xuQ(=ni66>X%0Y%8UAd|I9`iAG!~v1~xf9pLlPKq4f(=&Mt|1k%&8NQ@ zonEhyD0o7bYgN_N31?^0uzRU%;1Y53Ba%)Y)RXwJB!YLbo9a;6{?_WRmap3ztb{B) zlFk2#>bo4`Q+Ct^^JYfN_(u+UArlMS&77W(V=o!alAn}9txHyqpuIIXy2Lr)mho|T zyVLo?Dr%FbyK&$WybdX7p8^MpVpv?wEc&>^n^ZF!{qCJL&AZ&nn%8ylW&;7Ypyw)(Ae(sHUe<9hoEq<7OTeXrA_Yt|G$`FXjzJLoXG!RAV2xL8u82g8xJij<$&e1PgU_a$AWpJSR~Zg z!GWdU^W)(aU*RiYEGa{x^mTdp-EHJsiVNg#|1kkKJuYP;4JS_>7})#s2z@NkU*Evx z`RJ(D4+_w|J`f;8!uU6FK)j8pdAxrS578au(#OvX?8pBjEuiUuzY5w(6}fh9O`NtW|~=J>w^odEw9J@oGv zZ0Gf{d}Q7Dw($WG58PiF7@#8~r*vBG|5|!kW0EWRTL&1)%WTBoJMXV{er8N<;c$pV zqHj7KqM&bnK*HbDlI1L*q44hp&~&jFIK;y-(C(=LZKDGkKQ8l;6$l)H*yW8&e|(7O zk12d)dPkr%c(ttO{^K;qH<;oBvZ1w<<4W$hbc;p|-4`%m2Bzh=UIP>tsV7^EL$#SK;ST`_gr{sis^@fmX zeT43Luz`UA(5KE!w=>iK3(V*ck9XYPhx=oE{4=4?J+9Mn=a1c`df{%W%)l9#F*OpI z>4Td9(10P@8RVL-2BZMQlpm2adnwvaphM{GKlj7K-ONd#G+?&mVL7X`Y=0``!N7?*t8Zdqw}JvM2%2l;40l)u94{PUpc(i7anM9Be5dQ~pDN7q$;MaMzi|P9daht__@e$J^6rlZ*y__^g&9XEvleVN z_bmQ$n!Fj4l2#|ae!K>@@-<`h^z;@6k27@-416lgn7C9x`RU*o%v=@Ls+j`)y(``* zGG{n6se~x-p&D)+@zdle(DBPidVE>@^d8=}M4yZ!{!%me0ScU%OfNiPUIfbi0;7Iq zHDutZMcGq!1Ik2+3H-gjgS~SAhS(x`%d}nxAq$8W!Ctvx#&nF87nq{wN z{DIWDQ^#Gvz2JIoI4=D&!PpODl!-^OvarA~4kc;YvENGu!@|M{UGf`*LwtB8XEu_= zr3_3-)1LE4NnsY`4Geb!3bUbng2L^*e#}w`21a-1|;A2{lY?<_?4C59gcx& z`Nzodi+>d&Sy@YVO)ZOeO;^zZO`J-aBR-OIr6Z<-uRJkGaMK{0CVwN9ST_210YApF z_`sozyJiW}5tD0ch_s`iFZ`P_15v6M#b7I2TPobTU^a*bq-|*@#0if1mmqTJp_@xC zy-6n9P z%F4<*J1;OiW9R(MM6ybd>H>~4eR4fkO;#g)B!)uUvi<(IifZC76-EW+*~}Ep6S1O8 ztohXxbSKS>%A`uh6Qv$NI0%`CJ5Q6RgGW7uoqq?^Sqm)w_0^1@z>=u$@khewZQKt4CPMNs-sPS86M|E715BskAR>S-1^!D1_+kh2XB z9?emhx6O+-r)E_9eQ`Fo7P8(L&bHNMF6zAI7OByPgS!`LIKS)bvKqDGi14rD3HDPI zWfaI~*k}@cuG6ExjJ_xJHB3qvs<325x?U~Uuo52x>s7#E92X6D>cH)Bd3iawxf7%U z)06lhd2m9r@{pV-qtHsY*LXeUvRGC8-~2E~8W<(GN6n(5aGR*_}9PM-zS6fXtwH0nQ@?L#OR z>}*7TZ65%&lU1HGNmmahE~9!`4YPFelE!wGUrQ@GO5y@9{3$xZk27#%rp|_x6R3m^ z%zHCbr3`VVhKO^gJjO=tg@Ys3OB>7Cprf%Y8%+0j&qVpq=+WOiF@q|P-OTLxw z32X{qBKk*bwenIj)bN%APXBqP@H$t%WLlq=s0GTfIcTdjvMB=Ta&t%~!Oe?S7{MNk zOj^}2p?ZoHvQ8{FFLdq6XWX%4f|9}sq8RwDss9UKKGM%GDbG0In_4p_?+i1WP+2hb zhJL(trg;YEc4co1QKli=hhF7ny4F!V7emr@-t&}?&@(!=#XxV#1S^7F*R2uPumAex z!=1|G7c%x-sz?D`{#*M6^pf16w~&sM55v>Ym?wbIn`!oy3G zYwI7L9PKNBPaS87IvYRK+V90HbQkNfAn4wpkdz(eZFgBp$$&ILn`sA^k=Q+f$fCk0 z3vKL=r#kqMrcJt`?I!y;r{I)FqL9zY@ZVdA4!&tIGmg`)sC2!ct#hR}e#kU=h~A(l zrSE~zd+gL(aQ6U8Hhd5PeJg6bj;9~pdm0i>5)d37eztB*OExrtGj6`lZz4u1lUes= zn^jedTUIUV4JgEa-$**$0S0Un;tg^c)1Fp?L48=Eyrh)W2nEw81oF~9DM@r{+iF5rq?rbRs2<3=pAf|e8 zHJ!@I$r*NBmjC+qSTy@(?Qz7SqCBka-TxASSl9V*T{i|dK?{XeY9eXhGy( zC3_yCBpr>^zK;b1;DliJi6ErcAR6j3=q7go4B!$%N#S}cRaw)A&=?INrlASh1g)9? z)GLoa5qTEX%j9+kFooU%jhOc#8%aMkl)0z^UF*ZMk#&zNO(!~jWw!^S^_=N&+>qK$ z(0H1?q|@=Hwq4!zaw0YcYI-wds;(!+_@m?;0;fL|((b#g?DZ$YMN1Ho1Ox?p`}>0= zwwI)K&3Y8Iz#P_zJUK^-;{ox=&iwG6I3muX(HQ2W)038b{zuXg%NpwrU%;YH17$pYFaYH?RGfMu+zd|0Yy~vyfBan@hIqE7u5#-U$0f>?+s`mq5{0|z+V4LjcFv{D&#__K506oI7wWN#fx7>#y zo@8~cg8%${Gp_iR)vf;kq}}S+{ZsZ=>;R5>Ps@@aGq1{Jn*NQT_;hd_0WQl^d$tT| z&>p|#j|6rth-{S&9F%5#@U%Xr2bfK-I@1 zKygS;$wPdPj;mA}j_Zfp{J}SB=*9)33nibH`8R)~@FNn6<^9X(0#v!q^LIQU!+gcJ zoodDrS4)0Jqv1QWcKirSH1g~YUhHBpd5gQ!l<7Zy{1Dmg$o^}uR&!+fbltzkrFrHxo zn}mYzYZC(~1oHK{sH@mx0nCeO@W5QMcF^zog|TM%`$JSD(8cOn2L>D(&?Ed+ZmWWB z2x>7FY2E!(;Be&+N@!BP`VrK~t8)Bfpu^CVul8RCG7%)VU^1jp14R7+%`^rNO&UBQ z`L_g~;sB)goW#?neD`-~m(49L6{H|WKHGR)E5GmJ7YdBlz~i!jfvstjhYMn$z9U%X zG=M7||E}oYo`tz$T05MNukkFTC-WyBCVAAMPDGogx0>n513j|zsP-vPoBd$MQ=2}7 zhd>RL*}J3+;|Pw$+3(p`4)( zP;Bk`e*g%i<*6$3Lw|M#NCtL{Yl-^t-U;8IdNST&^fQzQ)adn5$bswHKFGlN{{R3C zl$YO}HglSALh-$}b1)v-!Z=X97|M4hq=oE?j75_~lUK93sLF%T z9==#4r)6MKLilsulT%te;RW19o=waX6Nru5by;t1!ZOyZma9T3XlmzzeH~6_ThZ9K zx3rIv_^R=-_mymTVcB2V! zRi2Er)%Ppr%^M8Q-sqI(Qrx)_Ri*KWl6y`?e^ih*EmRFIomb`ym0{GJ<0;Bqhc-yj z$opu_rl5xTuD|&&IZm7DUr+Tg{U`5uXpX_W1%-hPDUK2~+VU{Ts&4wVo8s9Jv8RQ~ z6>KGZjG;2xr%g}WC7ZZ_36N8Cp=vxOeOWpwOS9EmU1{50>bp~pg75zeN2r4y^(E}R zZC~#zcNx4#3_>#u1U@{DULp!H<1q8rvG*G^IL{mOUC6_#YA~aMtQPZjeN#40KV1VW z9`?xaW=X3KPN{0zq#0v^*mb$IB_j0}G|E69^oE%}pOpkY!O{lZ#GwSQs%Dus?%1a` z&b2f1Z*F^M-I~2WP@Hs8bhJ$)V6eUc^EQv+epvBs94dq-ps#ZaB^B28NGr&1D7a)h zcXwrDV*|XxK~u2dJxp>Y%Wtio&=Aw*^$F+~yVUpyG2mPXopSAz9OTd!aIyLLGq~8) zIk?#LDEQE;vSqG&sG}S4v4moJbSe#eg(B zQTcy1H#h0P&=a9$1-jQtpbNQ=0^CrnY}yMxy*o4%{8>HYDc3~Io$ld(5_NkYV7z5N zOb~PM(o80&TERQzaA7RdqU)-$1~)rbJ69SDOt05@>C5v@z#6XuP@{rNi+L*7WT7z|!{#HyNYAfCeB z=hPM6NIHBn>AiH57fdsDRnMek-b*8K;i<;#)gq0p<>3$COq8orRI9S`s>!r?B9vJc z*IHirg_$-a<_p?4tP{U*jg6DWvV+UZ`3Nysgk*GydCztH3FsiU-MD?u&@#2p?vr+H z^6ht_v;O$U@CIHftH>+|N0g>;5%0SL{U{w>Ld+P z&gY7~GCKr0kpj+N9D`P}nM*AEdUI|S^L&h2)LQcB#dkUi;)`)lz-V-NLaty=QIX*J zQ%iTs2>no&^HBJGY}dX0etB}{eoFSaEORK>wGo;W&*n|197u!vEz5O`ApwvCP*!Qk7 zupG9*P3CFo#6Zr))<#REbrCPd>W|2jgR7V}MG2qB0x*=~d00+@%J=%EW7o0l+YZ9j z@_t4vJA}@&DjQDOE>ggLVH78Y)#k=>e3ypS=hw<`3-t61DTSxw8%BKPy4 zeMOVf{E1d7DKj>)Rrt;3X`baRjZwepz?w@BGRjV~MAL(Yh@qWZ2rB#a!L1Xo_x)(e z_-Ka7`Dwnmz%~8n2{e`p{TSTJ6V}o9OQt;Ci$OxD4@0)w2vVZFwX=jaN16D;3+BBe zt&Brvwa3*=gm@fC4B78t=Z2WQ5l@>i3B0G`M>UjT?Bt_^=OYDKtS`qkw^@o|m2Mv> zIczxiAu4?7o*){EEmfWv3I@$NKBdRmI|R+r&UN5$GV7|ivubSDMuvyAwXQh_tqV*J zN0jHGB&c-yz#ALGc)k|E*7q++Tx4%7@>SG9_S1V0`&AJ@1jAQ+3Q1X;1c)lL9ZfH@ zFc`X$mo{Cb6*1z}hIXmV=dGlq6>O!s`Ks!#vDSndJq531o7_D}?qE}?L6(H|$7e~` zVU+dNQ0!;q_{nAr81lB606mnuKTNO}X=y*Ja|zD48k4cpCCSp^Asr`}2CkzN)tVO&^8bb|}Ny(E>l zidVFl+a~H_0}A;grN_8-XQvqE#naW*1yuVpA`{v@w+fFyZK&!kS$4_m+i6LAe|bQv za83T|L`TooyiTkByX$iktqRW7Y&{eDQ22?_+D4iLo6DmVnvEEFa0O;0egYh81CTW( z_ZWL^B`21TNBxaD4{|rYZVb57*$8^|sxFQ3P5iFcWZ2}f3C}SqVcDGydI$yj)aW>1kRlp-skfL^V!`B{xzM6P_U7O?O`ThX8F~bQ2<1em20xdtw zr^Q7L#?h0mPGmW*v+l;KMYRI7UpnzJ-3VfC8?>A&2Z=P_z9b^h=-JuXNq3uJzWa{E zOfvI)_pi5p$XZww-d%K@GTdf%SJoDm{vrY#0qrzU?ls$3$B`G{1NJciSZ{5HE~Uz)j0-6YKalCFVzb@ToH)`R;9* zS|%qa2c~U88x1a$s;p?Qw%+1SVyM($U&rXLGM)k_s2ygZt<50W?iCj#i+|AI=CW6X z;7+CG>}^ZfG4}GG86Az|a*}d96GI35jHG$;mS4(OZZQjK)+0=y-$;(S30jG55}buy z)y7^sClPV?vFzYJG(r-HR7@(D9AL79S#VLC7pPLC@-KMCQ4s}#b`8m%VaK_+xPZZY zhRF{@=eH@+#U*@@!wiZJeBd@`*{lH1VLYdW1 z|A*8%Yg$utb8{mjHfrystg$`TbQWY7($>|tCly9=AMO;r(mbgXJ&G)D+ah23eNBY?U7qd@9gYsY~-Wgsqcb@;M&t~8BPBh4c=!hUc!E#To~#? zDO@D1*U#;ih_oMBz68b%_Up1Xv7 zGSzX#8jAZ3$9p@m;ms(9^kYEQiO`=h~aP_*I9xfSRbgzh7ZJ^yhY3$ul{; zOYFyhE5nk&BacT7LCDz~TbKgB&ydg(>D{_}*=PO@k_X@fwLb*391p%v>(3n$TGmFI zu1&#bzpIv38iO^zwPOB;7RXLER8mHO-^8L%mj+$oyb|vT4q$Av;Nr^G-o_@80Rkx8 z8(c^Rq>Vh>?d+gMn~E*j2(DYKEx2p$FZL?}rt{lk&|m~T?&V{Yx3&Qd!%yT`-^lr0 zU6za=st}Et09@fP;LdcbNbeb-eQ!7g_AaQ4fZ>Ycu7^$yxB>U}CR^9a+x>(`gEC7l z6sf8?7wH7iDle1!wT1rX4xT6r%kNXZc48~5tIJtQdna_k?|R8ORQQGCh5H*WL%XMz zhu@!>U0Vn$v0EIK5Ol6s-*nua!{#X^cF#q5`8Gcwp7c_^fqUd1Y*; zC(>`OYk8`DnTY{h3y-(UWPwrf?imZge1F~_)P%8`QYNeTL5>IdpC{iVZ}%o&-;yAW ze;@!J57k&oNF7ZA9uIZ@sMxAP3SXu^!W?bHu>H>$!ChSI}psDwU z_(xVpH1rSngh#-%E zZ^yS2z}EzMI4OQHsy#=P8c+O@T+MHPwg^1ALtY2%KZyCy&{O-vGS4V!IXf|#nWh#N zaST7)2aIlzT*3d`)6p8N9>@Nml9M8c%H%laAEw`hz^hU)v0-a#YeS6y6I1)$%u=Rd zHdRMg%_yWG@XD7`Pt)kyNdm1Pp5r4C(aw4A`wKgo*4_!n{nI>e$N z)HLL8O`K90(#2`RHj?`1EMd?DML{wqoUS?}h(v2*M}jXAdd(l%!OzPfuM0W@Y(*9( zCK)+7mA6kYA!hU{%F00XTkC^QoxwXns2&+~ssI!-4;6)my)>0)^h9&Jff$?4LbOEj zr-Jh&rJ!}of>DN#&BoD? z=`qbooe+V*Jha8~m*Q3$;h!CuYFQdvm(Il*2t;q)k zKY_5ufP?ys6k4Ehi?i>k>viU)0`}P`J_9T|A;&#*(v2yak^tPjaJfH7xXo>Fe6D4vADRwYG?Tu;u|tqk4qtkQMwv z%6XqrXqf)3?azMO$;%=c{n>-akVTz#TRwg=2kWa!_k}51X&M-9A3b(>MmYi=s{Z`> zMCzH&8Z8F|TO~{al0q)(2^~nfD@v7v!h@q(yW9*JQ~zCXO0^-!!+zWAJkyIaZMN-Ldm1y#(k@#eQS?Noy%5e%))A28e@T2xY)76~F$`zCx%s2) zD0uh^3H^u+O1{Q#HHeV1yeK>v|Z11A#bo zfg{6M<<+x}u-Bb+m@JRgwEsu_Z$6aYHIrMfb(Q{V?J8=P7}2%ch-T|oty*dQS_jM7 zc-n4%nSN8T1X*KoS%Nu1Y#^mRJ6C1_8qW5At-WVdR9VnA+Mwtth8YzQ5io$LASg&` zLSp~~1e6StlqiyOY(NGbQ5uO#MoE%wlw<>qBD7@58N?<>Ns{5#X&A#h@3+3Y?z;NJ zwd`~D*`aprs;8=+y;{PBg5!X>(-PF^mJhLQ^DAj*^AdEg*DJMiq!LoqSk;Y=Kg~9q z&}~nO!1x92H0q&Dnu1HyJj=CqY|BWStD@VxC=zQ(jrjH1^1ZS#4N>F!tEnN~C?+Nb zl;PgkE0#sNSr8B}_oSmn#hro0QZfFR82Gnev4o}SUn87dxRw4HsqY>y3N(20f`y+d zF-+0rIPxsLP8{JS?q!j5v8S85(2oDGuCDw(?ABQQ8@ZyP9OUDJXvg(r9b&1bg^8_z zka16b$HTc`?cB6b%=uZjrGaNl+OaT=fAr_2G>*&xytj8TzIAN6+!>X2nZzAl`DB{a z)Sr8aZ=L*VeQh6|$Sak%lvJwp)a@Gv1&?kfGNUXZiT@HnAt;rKH z&&|(v)H<_fbL>eL^UEGSu>8c-nAI@W$oTjl$>HX*6R910OYM}_C1lArjJdzSrhV@E zfYw1P*CxlncPh?BJ(&4M0DhWJ(-)Tmw)A@_l7~Ow_48PN;DOc9PTNxgY+rI=hUJf| z*#7co=ysV~Urfc=V7pX3B^;Ezo~6j0E@ODnENDuulc*dvvK`i(dZ&=%TxMF8GK-y_ zlr*7^?`Gn0#JusnidAmM(4}2>J5)Zb6ck`1#YFroI_im=RoJqtt6&b(y#wA;CNY^K z`x8#=d$Yn<+L)=hVJw)L#=m00Z?3(e*_|?E5YL|GsXus?)PW}wJ>A{YIX>&obh3$A zJ3GHhreb~NRAwhmUy}M6FQ=wz>6MkKL-@WpZ6M~I*ypO%Cxk`={P3*1TW2XxLJ{TV zU_TyIk2gJuZUG?F>rFo^b!P5feP57msHc;H?QQ$S8L=HuC5)wg))p2S5Ge#Th66Q* zcDyj>`}pj`;#D55VSBW8a5=U3!#ySPpb&smpu`NJ0tqfH82DKD z?9(9);}f{1BGLP-xE}hjh&qE&3bhUww_X-@Z13u7I;e3<{Nd5;K5J(vFkR9icYLw_ z^DkQc0ikQupR0X#?$^aG8Bq+9ZOOH+(`T-tP=3fydk_Gn8A zbPMji{%bt`1;?MSA@2>+gv8J6MM)fizJ><}AM6=6rD|=we*&|s0k8DN{RTbC3*mu* z*I)r2H8wWpmiJ>kb8|1xnXK4zg*^ca3x-GfmoN~u6Izxb?b`ub7K(~XAe#5N z`u+P$S*=KW$g=Z8`h>QpB`{P`ethyVlr1c#q~SCEwB1-shGkxrQLKJ1WX2|>$9NrR z?SQg*DgMRU_YontF7C4w4Hnj<)W;u_B{*vnd#fDC@g`1|SH)8Dtk>?_^dsq>?bf>Up#c8rUFQkW-8Up&r1p4448Rr=#v7-9z zWKzJu&VU7Sf6AOI2lv}oS+0WR{7j^Ywl_kfUk?=?uj*2-N>K0?AM>ZRD3s`OyGb1R z*IXcwbN;m#RDJJUYW|))`Q~){#I;NVlAK!fe2r+hEpdie^vZy9l7x=ID80q>=e5xi&c$Zv)W6|q4o0!xbp|(>-i3Co|^8=bwuBjMbg&WZ>%~RuG26Q_l z4acO;ri~oTo@O|?5dGK0{VP8T^!S_RPp%ICk`VR{iod`N&-dAM=Q$JM~=4cc;y9#1itwghj z1URgsi`+x@3mAM#39pFs4*tt3x4qLc&F-yxtYn#Gn#o}CvU5hCPtFc@MdN{BA8SPX zUKmV(m+@Q{dl^c_5Qp{R>+7(ufn!(k*v%l_(R2AJIC(vqv;0b&#@t5s#AmKotW-%6 zq&sNGW%0)i-7R(1%-x*@34&+G+EO`o_8}A^cCcrADor7~8r?ticL3K1Ty!)uwqcJf zRIrJw>CrD@N6AS>HS~HXg!bjKadDz^``OI+2IcwNOm22fRD5_Q_;QI~Jm(f3Fy8)S z+COHY3G`CGx~+kM!TH0(nkaML&cK&7xEvrSQ0Wn70yxI@%t|%{Ps!Tcx$j;~B_}tQJ8Z#mjs_$ckSnJ3 z%>1on+4ZvDbzrlNn8Pak$N_HzEJVrQlPBK1>YBJ5GRWt=8jCvRcB=(HeaA>~-+d^| zF*UaEZ{5oT3g(+ckS-5*@O^&We}?NC;|29+dVWxLpbFw6GxxcX-bZfN~U%J;c$dA#R{oNa$QB{D?R- zcmOab9Ot*&4r}O3LTC4W#&Q4iTKOAP#nD2-`1i`>O%Fh&{hx(~cRX0J+HKM-kzy+f&B4jbJ=QQEr zVU23wN4<{4T6VunO!gLj#md1QyRU@*Com8sH0F=gov?28eRMofPP;DFm z3aXL2al0G>Ij~ri5RjyPE&)<;z}rDXe-k$3s@ybry1M$&V$UH_{!(%yxX}nLyTrD< zJFe7nS4elf`$=~#EGCe0iETL+R#vlG+e_q;4#Q}`gU=V;Pk#;jGT-`w@#m=pRtz>B zYzB7Zcf?dTegdXCBMKFV9NAQlIAyrN9zlx8PzZ_ckvM}1A&_6-Z$Aj^xSx=1{rXqs zx%HwqO#Bis0Bv^21=!F55TrmVsFEvNT^ZKkQlykSX#dI744e^M7`D}geJDs(fi`n; zfCK-c4m>?>9nDTOI%uQtKapU94A6Qj~5bPzl zO}PS4SRqu0)-8VsH$m@n4BDbEgDIvE5r&w zufhDt2Lb--BYQMOw$BM~{Bu;pf;01f=E^<}gxMbp+K_di)G!ej)D&v=LZL zAec-=bzf&63dXX!_mS$IN5~IZM++uq>zV3H-RJ%5=ItdwuR@e628H6Vxwadq?Y)mi zm+Cd(A>Fi*fw)-KyXfEh`QYo*CHo(zVoFc)rzIyVIXL}Fh;L3?bhHtcpAAi~1`7=* z!2Eu;Lc6u)dCYwBdI;>cluT%000@B~roG&JmMu9sd8Sf(mM7gEJC(=^JtIGKZE#`5 z&X}om$GRsZGd)i<7|IuU1~41z(WAq=P}T>SY18_zO;b&U&9xwBvV=-jJJ=YOjDNct>NHukH{+OlJJ|5 ze&{YHh@D)f>v2-cgLh*>(J~5e@+73y% zczZ6k7gfxs#q_$Z*cp(*ddVS#v@X?Txbk3HX@7~w(X;Q*qFe6G6a`PkQS=k%C6dDN zMAwzU+5EHYrTt?8R>X`WJThI2Ulj#|XR5EpkE*uc-zU7(E9ApBt0$X*oeg#qjOWqy zH%?utQ5h)B=L?RmrxO!qIFjgK>tsf*mebH#YaGMfu-2OCK~-t`Sk2Fk83!BixaT$1@-Y z!+Nhx6(s@XQ0ZG>UtV4^>`wb}@Lv21WwSCeLf5hxQghyRsW>WpeVyL*WR`p|q2P73 z)s3a$(z>sLbLH_GM28RAhT++`jCZ6;LS%Lt3fVGSbKry1p+gHCuk%SrpXSLr1QCgi zTib?Zdy(4a0tu{N@9;Q4^(2cV`aH9ce|&Roea zF4kQob52GDsJfkq7GCN~xvl(oyzR(>X^%>gjsvylG_4oU)_3FGMf#n<)0^AQ^_4?Z zezzJrcQIB4u&d_>l_j4==&4zjFExs^s62Tf-#pS5^e`@0V5AMbU|L9sAiQ%ufQN#w zxj7Y(_tKvY4!5cHafnL08e3SLTZlRw#8c2_mD)Hr@7HKmd5%+%ysC-*e#N7MkB|~n zU3<2(v$|Sq!So#g{@Ox3a)y}*K|wgf&cmLr$Lo?$_wEx8=|a%b*Z;(RdkHY=uE@Eu z+zYf0p)!W4j}J?>pNj8c{KP1trdV0@(P=rTw)R6(P}x^zw-h#0RWk+p$HiEF?6H*m z47-4GLRvu@CkJLDS|NE{sTuVJiqDXbKx738Tu~f^!n5|~fowH7_gVqW1kRKQgK>@N zrP9bchuxJ=^{ZA3nm)Z0Q3Lg1-0*juUeQwca(tFAVbR>yKQS`=ZSCUuoHjXD!0kvD z%WM`!ZWnuWYIJlm;M^3C+F!!3DL|)e*dHbSD?U;b7-a~|V8!?%P@79q+rH6&(izss zs>eGXWo4b3ni|`sag`JXJLFkJ^Fh-TERhJ_67bh9LOMQZ5NX$<9teq{v(*Iz1&K4E zS)Xp=+z%eCAne7T9&K-dnKNNIE+Q-(hQ$d0mxFe@}4@JcRJZOk6Mihu7LpQPrC zJR>9oTy1c+eG#S13=0&Rzf-HS(d;&g+k9fokx-bh?Hd69&;s$(f*erE0lHaqn*JeW zcRkjpRWFLaq~sRV3a6S&4T!gv}Wow zwrd>=O=I|Q8IAxuHAD8{wg@=p`{}%fcB5Wj-kk9_uwPU85wD&7wdra^`flc1v93m6 zcG96dwSWfXZeXSy-7-D$z5eLZyR?3=_jz;UvE>c33)7&S9b>hZMj{SwH~?TiX*n ze-r{cp+2}ol;^U6mW#RSk)H>Ca>STX&kY4-s#=i&mMiRiV7hTLY|XR9s$ZY z_^5o$?VjAC1NP&4z}!THR8Y`G^II$A#v$QM6iRV((p8qTroK@gH(a9|e`#Q1XJdoM zrU`>h7dKL&xaC;}Efea8zpL2ILRLwWfQ^%pz8Kqf!voIfh}FuOs} z2suae8JGzY&{OOpScyxe2`De4ID^}p6&BbR1`Kk##t{ENBA9^PJchCmq~;|2g#i=@ z2~X>$zrk3mGwOLD6()t~VYZsXwKaHV#H{*&tK zZ*E36j{kfSP2cy_b0<(O>&E{wfWL4G$#&M(aU5ogt8i%V)LXQ_Ehe&tOb&7ll9URm zc)7ekB;5p#SB@+xqxA}DSJz!!T{M4Q{AD~X_W_xLeKX+*HPA6MW8flyiiV8s*zPcv zDo3Eqhy?7bO7h2=3@on0t+|!e*rOxEZQ;NQU|Wx9 zM#&q1Vrc5NG$1=AM=HidrX4h?*xtud@i&tjg+)c18bc{J8?9eA$7{(muPl`cisQ5t z{GW*zFE|c1++crsPnP{&QG_06&!Aye4AFJ=js59z!e1Wu7vl3U1!B76&5_izCZ3aCm%@I%RRYjo-H2Is&V3TAbVv8#g36Isk%^BF&AcrG(kmHC}fp79{l zuB!;2>$o;A?VDA4iL?yYzdea?GOySvpPZtb)~^=&80g9*jWfD+RY>J3e+zoN zM2M$h0{%zwFXct!;kN0}hmAT6;o9zPSBY1I2rY1#R}8l3t6}K7K+T|Z2V@eRSIFZ` zkM4!?p$<}7+N|g8d@BcuOyCpFZHG0Xiq}Lt7KT9tB}R4XMTVt-hwVIx-4o-OE$={+ zH2opiE+*%svf%9E{*oh;FU^tpbjBN!%{FM;_rwA63?0Nh>fk*X`R2T3!c+XE2j-0s zAeUR}YdQ#Eza#<74e=QGOdBqy2%lDf2OI#fGPR) z>uH~?W*xt8x0?8Lh#ZhynIDc_mmz$zq4v?B9pBlSCdS6a!3uk&jCOkdi}wl*&8}z}OJDx{)&Q7# zDKHGot_7jS>y~BOJ%4r>7B91L?g8e7L{)#4CnTBf`6PLCJHQ z&M)#oNm8aCI`oK~Q^IjBWjQnZ@#+XiF6JC9(Tz#SyMUY`$eKaei$1sm%rHRneCVLW zJ&ID&xxkzvBRg2i8>0|vma9F|mIO-b+p0(+B^4vnJ@+)6Lhv#iO{<$THIRKSg0||i zNo@yykeys$Z%%_Y%W+=UX@r~}elqYmC>reRwtTj68Wjf>TS(6&R03o~29MaXp zd+Hy4+3bwV?WX8p%b?zqJzAn1F4T|*2h2v`0AV(_&_7oMbTX)n&B?d{4!azvRXsda zv*R-_r3>4FXxmR4l)5V8;|yR4sZn2ODKA};Nxt`#QI6#L02rROoQ}Uhkq`jIbK6iu zu73LEe>rKZ&bm^!)Z-_a{G}j54*m$3T7QC(@lzYp&fLq!<1auu{zcTWYg-2`G+10* z%UZ5+9Nj`o@nR?mr3Ob_fQ)%R!VE<@#MtlayPpdvn{eU!h(&a96iMPG4Y*RLNswF( zfiw#KSXx%{;9_WkAz#$xCB)m_ZrMPIH%S6wE{}cV4c1QZkpImWIt9W|cVL?-+ye4* zfFd(%UrS@@q~ZfbF@P1p1ZY87u_~p87c}5}U|~!aq+VqO#7Oy}jcSNaD6L1Ezc-wg zA23nE^3HEp^M>bE!(k#_<|xw>n^;=@}WD*r_SQX2)gYabm*4ebp5Zz%&g8 zJEf@T9Drn0aXMRODELwc*P3&1uZS2G%9Q8FYrwccNz#HT%msWEtc`;F{LhUjuP|UJ z*qmjJ6J`U8^lwZ4#aoa1OS#gc;{1O$4Zr#n76S4y&Fz6Q75ve-n5d{Iup+Lz}6Nl6E z9uO81iZc*gW4=tXJc4!{ zXv({;SmFte11vlhRuK$1QR__4eQ~hkG|HGEGKNVz#>RA#17$7Hp)~=k4l8YEt|q$09%0TTEyYv&u5;u2Ba6DvpVsM;GjSNT4IcH z!q8NHdciTZO&hqmE`ra~ZRvz0;$SNFPiR#tZpR?*F5ZnOy7M7T^ndTY9p{(5&XFXP;Bp~dT#-b=ysvx&n@r3 zL<7T`v=T0laT`*m4tqj^QMjc^@3thQORjbkSDZFhV>f0~Cquvu&+653hsX!3%p*ui za}xvdyNB|yccJrrN$a~gFOzb60IioLL_xk;(ccdNK*1hnUrgEYS@x&^;EeQ;DJ=yA z^okJoXrp&qU7QB*a3A45^tLD}wrQt_KcFt|#HVyy_9D%b{$q$A=Sqz<-8?cf0wI=t zA_sdnIz}Pu9m_@oD#!IWYHLq;{lz>+jm`}Py+P+NP+_tr1 zo81%G4xW`!tNY9XfoH!g=D~$Cw>+3oZx3y@GdwqEj}%uU6C|<8_QdRd1`bTuUA>Tr zvUd0m;QPHO)WoLGaULx!+zx#|sPPwC;B@(8400oF?|3NW{;*0ssS=_315y!x^p7dx zYgsIt<-A3)Ob=HP5on^QB!HR=S->Z*MJx;qI*@}7AwbO1$^{3yex*0a5l!u-M)Bro z58u4O{e|#n`dnG8=!C@lqh2tyK;2AiikQ@)5ZXoVzqtw)8Ha!8-MgLoAcm9dTQz3X)webKWUz0!Mjlm<@El_f7P9F*8R zLykXXQ5N%B2CM_KxN}27F)p`kJ^Ff%yx3nm&|&YbEwsgEW&#dx`%anZz3)3ZB1Wm2 z7V6g9F)MI)17vKtIyySiuSh#J8_!=oJgl*!G-uj#DN(P57>hc)`I5JNI#Rg-c^PO& zg=fT#QW165{(bR`d&+l1@@mt^zEd#CKpXa9AKDQcyUrRrgnI!$n*H}j^{L{OiG}s^ zPKRss&#MU)&lbs12K+D*!~iMV1TLPX=_RBS@8|uzBZRvCR;8qfw_PID2H=d&Ycp+h zu6bs8VyINUS|0>;0rkjGPA`I?jP=`+M%<}=f^du=CD;&tnsr<4KNA~ZbUq4}{>H)I zK4VjK2R;KlulzRoyO8W`Fd2Ms(Y>9$MRmQDSEj@mhkF%Kp}+T{j>LO7b8n{lp8qj$ zdoG{8j>Vjfkul2R{ML3o#GkaIA*3HoWm;v%Ee$Rt@aD$JTnQi`3 zeSp_K8*-leY@9p1-#?`;3>6=W`fO+mKOjk;W8Hn~8FW z*(u0Woip=c+4Tj8@%~+~A45-CLaVWjIDGSIWw4bE!&%H5B>xa5AIvhj>%v8!_Zkz6 zHCF4n>ucL@+?~X&$;`ZHmm!J!)i$o{cjeW~x6)c2z|MWcRPYwioMPQB?>`rLmsM=F ze!hRtYppaXQIjdT@0gA79_fXq49vU8qQ_%d>#r8EBWG6PV%c2;#0%VRS$yA_g?VC` zg4H_9Lg+GZ<*i?|@qbo`bu)^-ju5f|gTnagqC2w4U(kvY@{gz5Y((g!Rh0c)!aFgU z;~EtoR^_Lko41%A4fYLmJtQ;LeV!{yyiVJIwVXN9tlQI*+&7n;m})>8OA^VgAXko8 zJ70I{0i5|?xu?uJ`D=Ia)+ptTafH)m$?BeS%cY9tRqiv(LND2d?vUr?a}UP8E6n6B zzctg-p;9%XjH}SBOif!<_`W2)Tsbi}n0D@nrD^(E`24NHIqPo+qZzacTwQBwfDKs`&G))|VMx>!F!S+NG0q*#YJk2We142Ima2izcRJr)Spr^^IRj*nma z422A0c{@1J-$!=Zyw}Ycxt@NR1XsN+{ZtP%o%p5Q@;S;j31*`Q&%CS7$Yypht8cRn z=JPMV*??!))DVr;Y{0jdnN41iVQ+EOf(y!`0@7qivUaK42vNFO9D=Hcf#2|;CX85x zbQWCg1AF+1PT7U)W)3lhPIOM=JD<5e2u>=`y}LE^(QYT=S`X{srjjK&6%Wl}w7o=N z{O;n3i3+=LE4c{=5=kJpJRTFhLNxw?YFd`fNilg%vvS|hyScs@6o1Z%?`c$eV=&~d zEsw;9WtrOsAbx%=Q0?jX%{2qIywL&KPv7Uo!mG9GO|N?9WrOk4PiUM{*YUkf->3(7 zh!KE*;2x;df0Z$PH(usZ;*pfM204nltyX-QpUTXYTKFgI5`d2T`}ttUiaTi9u?Zji zfqUZT21&Zw+0*2R0s{txBdQKbyK8xsQb)ZS{_)Gbg3)7#(7`~;d90V3WwTy})L<}v z`k#B>%Vq_6RJ&xi8)vOvEI1MJB(1f_jfLZDvQ=t$iWc9+1eflAv=(!cV|9xc-g5@i z%u)IEsF)S<7R#Kub*%kiRq}(HQ-pI1X{=V+c>{zaz4rV)5$Qoa`u#HBi|52YQv$UrXFz_GVn~u?3)|Dt>ia1&7QkmI=Pr-qEqUZjaHQ*UEZ6t-hqvb%WDW?BHrxV;mzMGqu$Ij8f8W#!- zV9n!!ONVJyrn8^1V5=wn`WdFxFT7fdjdE*U4S0)hixtY^tX29#{BXP_ zNij!ycA7m}RL(M!IrD0Gf7EE>6B&6i*Tq=NLbMF&L)ps)b?@a%qjgb(;)?h3D?=(c z0$3C@IlJBJKU4%QTU8{yLvPQH zZOw@eP!Lq1{%H4nrg3<@K=$PaW1>dlK##1h;xlrC3pf3!vgy|2*=Yfb?z`{r;ti6= z&1}p0MY5SiTp}+pcb>5FToCuffQ4pOxL_DY?Ao{e9~{)lLB&w9Gh`>4HSGrY25Or| zi$3194F*jCMY(Q4?>dwZ87EzfP>Y>a;6h zEB&-mu9BpXavKdj75i~u_yp!=#Lajs5mv94f$N*aFEo)ve6ayGt5WULEB;{f^e*4E zx9E{!KV@}J?b)pS!|4!*WSXJ=i+ArzM$$je)l?>_O^x>JzVu0kV)A@k-qTyp>PWe} znXAB*DRx}lG=IJ|)!`Dak^Tc5>4weE3^ooUKa?;efgM-5=*=GGX6gXP|WUC(yCRQycjij5~c(Nr?g!Z?PCgu0b)T@!Pgjtx__P$Vq}jEP%^j%2}dmfP(@gYfC+AhG(@oj|6?;# zOvf*?U^@Nw_`*c5$KwS$zQYQX?)>(x{Mv(Cg87?uYV}RpGfL!v@%h7P%*3UdUWyg} zn|;fQn8Vsk+C#fu{oZ@Z?AlCQ>TO`DU;S>={~!A2w6uaLw41*)c|PL3e!4Yc`u)~_ z(!3vH_WozM|37#E#Mze4|JP?!)*M=IVf7|URhm3UYYI* z_WtiuZyAEFW&dNy(ui~5pT)fK?!WK+6yM*L>=%LiZG|b1Pjl*dsQ!0B^ft<^JZ5KZ zZ_T`QG{+AQ0&bs?+E$fYP#{C9eYB|swq)6MSOyRh*!=F#sAzZiNUpz-1g}Xy-JRk# zQNR~e6rDZ^yW-RfTn>94C?j421(l;M(WG+ZYIV7@HCGh39#y;hwZTr$IZ}ZE;)--k zNA|8-{PbTbKQH|>m^WQlUp{=Jl@py8{<%7r1}@3TFv!VACBMqua*6i9LMb+MI;d+FMp* zh_|geLDU>$pqLwgzn5HxG{DRaI+T<<6i>Sg`D;vzP<={HjXYX=js`oJ<;ptDViZ)|N7$iW_@RHQm+VXVaVS()fdQ{dz5F{X= zGyd_BAKGkd{K(_Bvx=VICOutdiS>CI8Qn`h(gidfg8dgN<7;c8(2xVNSDY0QL2A^W zuQv4?zlzVJE_eQ*gKr*gEJKg)yh6HxXl2TzeRVC{Y`6JZj|JzYNW!BRJK$30pK~!W z)iX3jN_}zUdjDYBaf>7?Zp|;XtfJ`E){RqZE?Oh+M(Y{{v6 z7LHakyV0w5<9qA9h|HjIFZbgU$;Ar&^a410_oRW$p^&bd7i^5!@DV2oD&2TKQOWoF z5bT9A@Y84kMs%ud#5cj>6}QoruPc7Rmsjf6dpy@UH^>$#LZJ~dg7hL54^$1Q9D=#B zil2%F+J|g}`uEdw<4Qf(teUixGG;WI#43{vb=t4FR`lb*P8my${M-Bn{nNlGy-@iS z9z)s!Iuqx@&-w#&YiE1ok0e zf5tF2ZS*k{4M{2VNjsAp$v8uuUA0~EWr18#V{QG0=9a}rDjZ&aTvfdJWm8il^2i*y zM^*VUFBcVArqfkb5hovAUpmnmUU0iG-Y}0-R;Ybq?z;)L@c91B+3i~^C(atV zCsav__$@7qv6Dxp7~Mo~tikT2_r@We?T+nb)oV3j9xTny(iPQcVY99YbOYxDgl_a- z4N>v=99^r5Yd^i*{sPr3Wgl0O$vtWPllO9Qp~ufeCM2COTFDWy8lZ<_XI{G$=3C!! z1D`3r6rjPVKbrD97tXD<{Smzf`Z(L zZ>MoV;e1Z!jU=WS#0!ALb`*7)Lrl;MTe<#0^*t#(IkJ-71Lb?tUInHLnUGGMTqD^U zk}KTF%cH@orjtX7|XEKt*?PGlaxQNV6H%UK@Xq$gF=ts-EoA-UH;w^rCt zxy&N0J22u*17Bll8=eo-^OR(Toz2@i$3XjioyAvBUQUyq5j1F+RuhAF%X!f)`8C-h zvW}h3-lxM;Ea?hJ{GH^zE)?6~hhE8mZ>Eu2Ckqo>Qk_dJEURDpJ#j&+weSlhn5RMzkAe6e-Zaa% zlqh0}tX8V)HE%-x$hkPyyO?36D_+ek#dr7#-M(M#tl&2pudBzGr2m4oLe)yR*f0DK z>ad%Dz5xvew)Lsg?Kb?;yJ{xPGTx0C2YSp(a@+<&56HJ(5vpYpD=PUC2 zl;J>LGtFT9*eUZJ(@{hyFpse}9}Vmg6jdT`leyjwNs;asbjR1DTYCd0xv@cfEOdi) zucryix#^YcX{^U?eEi`W!5W-PygwAJ+vI$+Vb1ai6HlW~-Pb=t!WWFZd-4w}Cu_vc zm*2do`(>RuCH{0cIpAAcQ_wuT5CQM;hiZEMijGJ-~$YY?& z0ee9%@m0 zGu}-wWZmq?i~?zsXqVX8aVEhtIt~5{&ZUCC8M>xtu88ka6$uqfQHzQCgAO;cT>l0{_x46=auVok9xaW2HW#5jRZCKo+)`k5$a#8B-0e_GyW;*m4$t3 zlw!+RYte=wgW!XaJL?~woAoy|=U{%i+?@gCmX4!K<&PT9S0x+NNNX)ij80fr6dn4Y zRFop*#NyoI&!EbsIUh|8qJyBE2(%5NohW-h*?RrF2i>ctYZ=cATyG5W%BceH!Ctfg z!`}znoy1!`6O8sJ^z3`KBSBXOaF?PK=N^9s6>iOSXo}@`7lHbm>H*P$Hz8(9_stqa zjS>&(wFp@zuxWFmTdY}fUNlTpJBtehdLEm1%s+8Vc_MZ4;ym})0>Kz%6PZ${-RH@! z;D}MeH$X}q#=A41GL`M^^-xt!+0^4{YJzXOp2+3~yZyn>dRKQDCa^CxnH~6qJp;vt zL>J26nDKq-R=I7xt-$jpjWL5TTW)UA9HZb=RG#x((@=c7_&{ZwS3m|L$X;#<%D!IIA)H#C#&suFRYX|uWAu43{X)^QFi%a$8^?~ z#}=0u^u@&{+&+4`0~63Qm7<|UXRjWn&Uu!HTbbd$UzTs0)#q~7vXFW?j;?rs@>Li>?V|_9+v`Q@j)2u!kGGRJfZvpI0^5l>4VmNoLIA z8)4^OMbyr}w)Z*yL;}hSamH#l5hEHc2xp()OTogh18G^Kn;vCcBIv$G_x~kyhlpF( zgocOY%gTB}8pDp)S2s7z53~K{kI><-rc3oEuDW$pM05OlRG9s57!c(U@!LWLe^_Cg z5#qwm)*qUG^ZeNK!x208e{;bRmqT$Qpl)Mm<6G^@Fx@^$+x3suff8Vig4#z^3CI8CUsfoF-`I6Y WG^SpOPV+eOij1Vf)y&KK_x~Stl|~)_ literal 0 HcmV?d00001 diff --git a/libs/clipboard/src/platform/unix/macos/paste_observer.rs b/libs/clipboard/src/platform/unix/macos/paste_observer.rs new file mode 100644 index 00000000000..01e8b6c10df --- /dev/null +++ b/libs/clipboard/src/platform/unix/macos/paste_observer.rs @@ -0,0 +1,179 @@ +use super::pasteboard_context::PasteObserverInfo; +use fsevent::{self, StreamFlags}; +use hbb_common::{bail, log, ResultType}; +use std::{ + sync::{ + mpsc::{channel, Receiver, RecvTimeoutError, Sender}, + Arc, Mutex, + }, + thread, + time::Duration, +}; + +enum FseventControl { + Start, + Stop, + Exit, +} + +struct FseventThreadInfo { + tx: Sender, + handle: thread::JoinHandle<()>, +} + +pub struct PasteObserver { + exit: Arc>, + observer_info: Arc>>, + tx_handle_fsevent_thread: Option, + handle_observer_thread: Option>, +} + +impl Drop for PasteObserver { + fn drop(&mut self) { + *self.exit.lock().unwrap() = true; + if let Some(handle_observer_thread) = self.handle_observer_thread.take() { + handle_observer_thread.join().ok(); + } + if let Some(tx_handle_fsevent_thread) = self.tx_handle_fsevent_thread.take() { + tx_handle_fsevent_thread.tx.send(FseventControl::Exit).ok(); + tx_handle_fsevent_thread.handle.join().ok(); + } + } +} + +impl PasteObserver { + const OBSERVE_TIMEOUT: Duration = Duration::from_secs(30); + + pub fn new() -> Self { + Self { + exit: Arc::new(Mutex::new(false)), + observer_info: Default::default(), + tx_handle_fsevent_thread: None, + handle_observer_thread: None, + } + } + + pub fn init(&mut self, cb_pasted: fn(&PasteObserverInfo) -> ()) -> ResultType<()> { + let Some(home_dir) = dirs::home_dir() else { + bail!("No home dir is set, do not observe."); + }; + + let (tx_observer, rx_observer) = channel::(); + let handle_observer = Self::init_thread_observer( + self.exit.clone(), + self.observer_info.clone(), + rx_observer, + cb_pasted, + ); + self.handle_observer_thread = Some(handle_observer); + let (tx_control, rx_control) = channel::(); + let handle_fsevent = Self::init_thread_fsevent( + home_dir.to_string_lossy().to_string(), + tx_observer, + rx_control, + ); + self.tx_handle_fsevent_thread = Some(FseventThreadInfo { + tx: tx_control, + handle: handle_fsevent, + }); + Ok(()) + } + + #[inline] + fn get_file_from_path(path: &String) -> String { + let last_slash = path.rfind('/').or_else(|| path.rfind('\\')); + match last_slash { + Some(index) => path[index + 1..].to_string(), + None => path.clone(), + } + } + + fn init_thread_observer( + exit: Arc>, + observer_info: Arc>>, + rx_observer: Receiver, + cb_pasted: fn(&PasteObserverInfo) -> (), + ) -> thread::JoinHandle<()> { + thread::spawn(move || loop { + match rx_observer.recv_timeout(Duration::from_millis(300)) { + Ok(event) => { + if (event.flag & StreamFlags::ITEM_CREATED) != StreamFlags::NONE + && (event.flag & StreamFlags::ITEM_REMOVED) == StreamFlags::NONE + && (event.flag & StreamFlags::IS_FILE) != StreamFlags::NONE + { + let source_file = observer_info + .lock() + .unwrap() + .as_ref() + .map(|x| Self::get_file_from_path(&x.source_path)); + if let Some(source_file) = source_file { + let file = Self::get_file_from_path(&event.path); + if source_file == file { + if let Some(observer_info) = observer_info.lock().unwrap().as_mut() + { + observer_info.target_path = event.path.clone(); + cb_pasted(observer_info); + } + } + } + } + } + Err(_) => { + if *(exit.lock().unwrap()) { + break; + } + } + } + }) + } + + fn new_fsevent(home_dir: String, tx_observer: Sender) -> fsevent::FsEvent { + let mut evt = fsevent::FsEvent::new(vec![home_dir.to_string()]); + evt.observe_async(tx_observer).ok(); + evt + } + + fn init_thread_fsevent( + home_dir: String, + tx_observer: Sender, + rx_control: Receiver, + ) -> thread::JoinHandle<()> { + log::debug!("fsevent observe dir: {}", &home_dir); + thread::spawn(move || { + let mut fsevent = None; + loop { + match rx_control.recv_timeout(Self::OBSERVE_TIMEOUT) { + Ok(FseventControl::Start) => { + if fsevent.is_none() { + fsevent = + Some(Self::new_fsevent(home_dir.clone(), tx_observer.clone())); + } + } + Ok(FseventControl::Stop) | Err(RecvTimeoutError::Timeout) => { + let _ = fsevent.as_mut().map(|e| e.shutdown_observe()); + fsevent = None; + } + Ok(FseventControl::Exit) | Err(RecvTimeoutError::Disconnected) => { + break; + } + } + } + log::info!("fsevent thread exit"); + let _ = fsevent.as_mut().map(|e| e.shutdown_observe()); + }) + } + + pub fn start(&mut self, observer_info: PasteObserverInfo) { + if let Some(tx_handle_fsevent_thread) = self.tx_handle_fsevent_thread.as_ref() { + self.observer_info.lock().unwrap().replace(observer_info); + tx_handle_fsevent_thread.tx.send(FseventControl::Start).ok(); + } + } + + pub fn stop(&mut self) { + if let Some(tx_handle_fsevent_thread) = &self.tx_handle_fsevent_thread { + self.observer_info = Default::default(); + tx_handle_fsevent_thread.tx.send(FseventControl::Stop).ok(); + } + } +} diff --git a/libs/clipboard/src/platform/unix/macos/paste_task.rs b/libs/clipboard/src/platform/unix/macos/paste_task.rs new file mode 100644 index 00000000000..33a11ed6c6e --- /dev/null +++ b/libs/clipboard/src/platform/unix/macos/paste_task.rs @@ -0,0 +1,639 @@ +use crate::{ + platform::unix::{FileDescription, FileType, BLOCK_SIZE}, + send_data, ClipboardFile, CliprdrError, ProgressPercent, +}; +use hbb_common::{allow_err, log, tokio::time::Instant}; +use std::{ + cmp::min, + fs::{File, FileTimes}, + io::{BufWriter, Write}, + os::macos::fs::FileTimesExt, + path::{Path, PathBuf}, + sync::{ + mpsc::{Receiver, RecvTimeoutError}, + Arc, Mutex, + }, + thread, + time::{Duration, SystemTime}, +}; + +const RECV_RETRY_TIMES: usize = 3; + +const DOWNLOAD_EXTENSION: &str = "rddownload"; +const RECEIVE_WAIT_TIMEOUT: Duration = Duration::from_millis(5_000); + +// https://stackoverflow.com/a/15112784/1926020 +// "1984-01-24 08:00:00 +0000" +const TIMESTAMP_FOR_FILE_PROGRESS_COMPLETED: u64 = 443779200; +const ATTR_PROGRESS_FRACTION_COMPLETED: &str = "com.apple.progress.fractionCompleted"; + +pub struct FileContentsResponse { + pub conn_id: i32, + pub msg_flags: i32, + pub stream_id: i32, + pub requested_data: Vec, +} + +#[derive(Debug)] +struct PasteTaskProgress { + // Use list index to identify the file + // `list_index` is also used as the stream id + list_index: i32, + offset: u64, + total_size: u64, + current_size: u64, + last_sent_time: Instant, + download_file_index: i32, + download_file_size: u64, + download_file_path: String, + download_file_current_size: u64, + file_handle: Option>, + error: Option, + is_canceled: bool, +} + +struct PasteTaskHandle { + progress: PasteTaskProgress, + target_dir: PathBuf, + files: Vec, +} + +pub struct PasteTask { + exit: Arc>, + handle: Arc>>, + handle_worker: Option>, +} + +impl Drop for PasteTask { + fn drop(&mut self) { + *self.exit.lock().unwrap() = true; + if let Some(handle_worker) = self.handle_worker.take() { + handle_worker.join().ok(); + } + } +} + +impl PasteTask { + const INVALID_FILE_INDEX: i32 = -1; + + pub fn new(rx_file_contents: Receiver) -> Self { + let exit = Arc::new(Mutex::new(false)); + let handle = Arc::new(Mutex::new(None)); + let handle_worker = + Self::init_worker_thread(exit.clone(), handle.clone(), rx_file_contents); + Self { + handle, + exit, + handle_worker: Some(handle_worker), + } + } + + pub fn start(&mut self, target_dir: PathBuf, files: Vec) { + let mut task_lock = self.handle.lock().unwrap(); + if task_lock + .as_ref() + .map(|x| !x.is_finished()) + .unwrap_or(false) + { + log::error!("Previous paste task is not finished, ignore new request."); + return; + } + let total_size = files.iter().map(|f| f.size).sum(); + let mut task_handle = PasteTaskHandle { + progress: PasteTaskProgress { + list_index: -1, + offset: 0, + total_size, + current_size: 0, + last_sent_time: Instant::now(), + download_file_index: Self::INVALID_FILE_INDEX, + download_file_size: 0, + download_file_path: "".to_owned(), + download_file_current_size: 0, + file_handle: None, + error: None, + is_canceled: false, + }, + target_dir, + files, + }; + task_handle.update_next(0).ok(); + if task_handle.is_finished() { + task_handle.on_finished(); + } else { + if let Err(e) = task_handle.send_file_contents_request() { + log::error!("Failed to send file contents request, error: {}", &e); + task_handle.on_error(e); + } + } + *task_lock = Some(task_handle); + } + + pub fn cancel(&self) { + let mut task_handle = self.handle.lock().unwrap(); + if let Some(task_handle) = task_handle.as_mut() { + task_handle.progress.is_canceled = true; + task_handle.on_cancelled(); + } + } + + fn init_worker_thread( + exit: Arc>, + handle: Arc>>, + rx_file_contents: Receiver, + ) -> thread::JoinHandle<()> { + thread::spawn(move || { + let mut retry_count = 0; + loop { + if *exit.lock().unwrap() { + break; + } + + match rx_file_contents.recv_timeout(Duration::from_millis(300)) { + Ok(file_contents) => { + let mut task_lock = handle.lock().unwrap(); + let Some(task_handle) = task_lock.as_mut() else { + continue; + }; + if task_handle.is_finished() { + continue; + } + + if file_contents.stream_id != task_handle.progress.list_index { + // ignore invalid stream id + continue; + } else if file_contents.msg_flags != 0x01 { + retry_count += 1; + if retry_count > RECV_RETRY_TIMES { + task_handle.progress.error = Some(CliprdrError::InvalidRequest { + description: format!( + "Failed to read file contents, stream id: {}, msg_flags: {}", + file_contents.stream_id, + file_contents.msg_flags + ), + }); + } + } else { + let resp_list_index = file_contents.stream_id; + let Some(file) = &task_handle.files.get(resp_list_index as usize) + else { + // unreachable + // Because `task_handle.progress.list_index >= task_handle.files.len()` should always be false + log::warn!( + "Invalid response list index: {}, file length: {}", + resp_list_index, + task_handle.files.len() + ); + continue; + }; + if file.conn_id != file_contents.conn_id { + // unreachable + // We still add log here to make sure we can see the error message when it happens. + log::error!( + "Invalid response conn id: {}, expected: {}", + file_contents.conn_id, + file.conn_id + ); + continue; + } + + if let Err(e) = task_handle.handle_file_contents_response(file_contents) + { + log::error!("Failed to handle file contents response: {}", &e); + task_handle.on_error(e); + } + } + + if !task_handle.is_finished() { + if let Err(e) = task_handle.send_file_contents_request() { + log::error!("Failed to send file contents request: {}", &e); + task_handle.on_error(e); + } + } else { + retry_count = 0; + task_handle.on_finished(); + } + } + Err(RecvTimeoutError::Timeout) => { + let mut task_lock = handle.lock().unwrap(); + if let Some(task_handle) = task_lock.as_mut() { + if task_handle.check_receive_timemout() { + retry_count = 0; + task_handle.on_finished(); + } + } + } + Err(RecvTimeoutError::Disconnected) => { + break; + } + } + } + }) + } + + pub fn is_finished(&self) -> bool { + self.handle + .lock() + .unwrap() + .as_ref() + .map(|handle| handle.is_finished()) + .unwrap_or(true) + } + + pub fn progress_percent(&self) -> Option { + self.handle + .lock() + .unwrap() + .as_ref() + .map(|handle| handle.progress_percent()) + } +} + +impl PasteTaskHandle { + fn update_next(&mut self, size: u64) -> Result<(), CliprdrError> { + if self.is_finished() { + return Ok(()); + } + self.progress.current_size += size; + + let is_start = self.progress.list_index == -1; + if is_start || (self.progress.offset + size) >= self.progress.download_file_size { + if !is_start { + self.on_done(); + } + for i in (self.progress.list_index + 1)..self.files.len() as i32 { + let Some(file_desc) = self.files.get(i as usize) else { + return Err(CliprdrError::InvalidRequest { + description: format!("Invalid file index: {}", i), + }); + }; + match file_desc.kind { + FileType::File => { + if file_desc.size == 0 { + if let Some(new_file_path) = + Self::get_new_filename(&self.target_dir, file_desc) + { + if let Ok(f) = std::fs::File::create(&new_file_path) { + f.set_len(0).ok(); + Self::set_file_metadata(&f, file_desc); + } + }; + } else { + self.progress.list_index = i; + self.progress.offset = 0; + self.open_new_writer()?; + break; + } + } + FileType::Directory => { + let path = self.target_dir.join(&file_desc.name); + if !path.exists() { + std::fs::create_dir_all(path).ok(); + } + } + FileType::Symlink => { + // to-do: handle symlink + } + } + } + } else { + self.progress.offset += size; + self.progress.download_file_current_size += size; + self.update_progress_completed(None); + } + if self.progress.file_handle.is_none() { + self.progress.list_index = self.files.len() as i32; + self.progress.offset = 0; + self.progress.download_file_size = 0; + self.progress.download_file_current_size = 0; + } + Ok(()) + } + + fn start_progress_completed(&self) { + if let Some(file) = self.progress.file_handle.as_ref() { + let creation_time = + SystemTime::UNIX_EPOCH + Duration::from_secs(TIMESTAMP_FOR_FILE_PROGRESS_COMPLETED); + file.get_ref() + .set_times(FileTimes::new().set_created(creation_time)) + .ok(); + xattr::set( + &self.progress.download_file_path, + ATTR_PROGRESS_FRACTION_COMPLETED, + "0.0".as_bytes(), + ) + .ok(); + } + } + + fn update_progress_completed(&mut self, fraction_completed: Option) { + let fraction_completed = fraction_completed.unwrap_or_else(|| { + let current_size = self.progress.download_file_current_size as f64; + let total_size = self.progress.download_file_size as f64; + if total_size > 0.0 { + current_size / total_size + } else { + 1.0 + } + }); + xattr::set( + &self.progress.download_file_path, + ATTR_PROGRESS_FRACTION_COMPLETED, + &fraction_completed.to_string().as_bytes(), + ) + .ok(); + } + + #[inline] + fn remove_progress_completed(path: &str) { + if !path.is_empty() { + xattr::remove(path, ATTR_PROGRESS_FRACTION_COMPLETED).ok(); + } + } + + fn open_new_writer(&mut self) -> Result<(), CliprdrError> { + let Some(file) = &self.files.get(self.progress.list_index as usize) else { + return Err(CliprdrError::InvalidRequest { + description: format!( + "Invalid file index: {}, file count: {}", + self.progress.list_index, + self.files.len() + ), + }); + }; + + let original_file_path = self + .target_dir + .join(&file.name) + .to_string_lossy() + .to_string(); + let Some(download_file_path) = Self::get_first_filename( + format!("{}.{}", original_file_path, DOWNLOAD_EXTENSION), + file.kind, + ) else { + return Err(CliprdrError::CommonError { + description: format!("Failed to get download file path: {}", original_file_path), + }); + }; + let Some(download_path_parent) = Path::new(&download_file_path).parent() else { + return Err(CliprdrError::CommonError { + description: format!( + "Failed to get parent of the download file path: {}", + original_file_path + ), + }); + }; + if !download_path_parent.exists() { + if let Err(e) = std::fs::create_dir_all(download_path_parent) { + return Err(CliprdrError::FileError { + path: download_path_parent.to_string_lossy().to_string(), + err: e, + }); + } + } + match std::fs::File::create(&download_file_path) { + Ok(handle) => { + let writer = BufWriter::with_capacity(BLOCK_SIZE as usize * 2, handle); + self.progress.download_file_index = self.progress.list_index; + self.progress.download_file_size = file.size; + self.progress.download_file_path = download_file_path; + self.progress.download_file_current_size = 0; + self.progress.file_handle = Some(writer); + self.start_progress_completed(); + } + Err(e) => { + self.progress.error = Some(CliprdrError::FileError { + path: download_file_path, + err: e, + }); + } + }; + Ok(()) + } + + fn get_first_filename(path: String, r#type: FileType) -> Option { + let p = Path::new(&path); + if !p.exists() { + return Some(path); + } else { + for i in 1..9999999 { + let new_path = match r#type { + FileType::File => { + if let Some(ext) = p.extension() { + let new_name = format!( + "{}-{}.{}", + p.file_stem().unwrap_or_default().to_string_lossy(), + i, + ext.to_string_lossy() + ); + p.with_file_name(new_name).to_string_lossy().to_string() + } else { + format!("{} ({})", path, i) + } + } + FileType::Directory => format!("{} ({})", path, i), + FileType::Symlink => { + // to-do: handle symlink + return None; + } + }; + if !Path::new(&new_path).exists() { + return Some(new_path); + } + } + } + // unreachable + None + } + + fn progress_percent(&self) -> ProgressPercent { + let percent = self.progress.current_size as f64 / self.progress.total_size as f64; + ProgressPercent { + percent, + is_canceled: self.progress.is_canceled, + is_failed: self.progress.error.is_some(), + } + } + + fn is_finished(&self) -> bool { + self.progress.is_canceled + || self.progress.error.is_some() + || self.progress.list_index >= self.files.len() as i32 + } + + fn check_receive_timemout(&mut self) -> bool { + if !self.is_finished() { + if self.progress.last_sent_time.elapsed() > RECEIVE_WAIT_TIMEOUT { + self.progress.error = Some(CliprdrError::InvalidRequest { + description: "Failed to read file contents".to_string(), + }); + return true; + } + } + false + } + + fn on_finished(&mut self) { + if self.progress.error.is_some() { + self.on_cancelled(); + } else { + self.on_done(); + } + if self.progress.current_size != self.progress.total_size { + self.progress.error = Some(CliprdrError::InvalidRequest { + description: "Failed to download all files".to_string(), + }); + } + } + + fn on_error(&mut self, error: CliprdrError) { + self.progress.error = Some(error); + self.on_cancelled(); + } + + fn on_cancelled(&mut self) { + self.progress.file_handle = None; + std::fs::remove_file(&self.progress.download_file_path).ok(); + } + + fn on_done(&mut self) { + self.update_progress_completed(Some(1.0)); + Self::remove_progress_completed(&self.progress.download_file_path); + + let Some(file) = self.progress.file_handle.as_mut() else { + return; + }; + if self.progress.download_file_index == PasteTask::INVALID_FILE_INDEX { + return; + } + + if let Err(e) = file.flush() { + log::error!("Failed to flush file: {:?}", e); + } + self.progress.file_handle = None; + + let Some(file_desc) = self.files.get(self.progress.download_file_index as usize) else { + // unreachable + log::error!( + "Failed to get file description: {}", + self.progress.download_file_index + ); + return; + }; + let Some(rename_to_path) = Self::get_new_filename(&self.target_dir, file_desc) else { + return; + }; + match std::fs::rename(&self.progress.download_file_path, &rename_to_path) { + Ok(_) => Self::set_file_metadata2(&rename_to_path, file_desc), + Err(e) => { + log::error!("Failed to rename file: {:?}", e); + } + } + self.progress.download_file_path = "".to_owned(); + self.progress.download_file_index = PasteTask::INVALID_FILE_INDEX; + } + + fn get_new_filename(target_dir: &PathBuf, file_desc: &FileDescription) -> Option { + let mut rename_to_path = target_dir + .join(&file_desc.name) + .to_string_lossy() + .to_string(); + if Path::new(&rename_to_path).exists() { + let Some(new_path) = Self::get_first_filename(rename_to_path.clone(), file_desc.kind) + else { + log::error!("Failed to get new file name: {}", &rename_to_path); + return None; + }; + rename_to_path = new_path; + } + Some(rename_to_path) + } + + #[inline] + fn set_file_metadata(f: &File, file_desc: &FileDescription) { + let times = FileTimes::new() + .set_accessed(file_desc.atime) + .set_modified(file_desc.last_modified) + .set_created(file_desc.creation_time); + f.set_times(times).ok(); + } + + #[inline] + fn set_file_metadata2(path: &str, file_desc: &FileDescription) { + let times = FileTimes::new() + .set_accessed(file_desc.atime) + .set_modified(file_desc.last_modified) + .set_created(file_desc.creation_time); + File::options() + .write(true) + .open(path) + .map(|f| f.set_times(times)) + .ok(); + } + + fn send_file_contents_request(&mut self) -> Result<(), CliprdrError> { + if self.is_finished() { + return Ok(()); + } + + let stream_id = self.progress.list_index; + let list_index = self.progress.list_index; + let Some(file) = &self.files.get(list_index as usize) else { + // unreachable + return Err(CliprdrError::InvalidRequest { + description: format!("Invalid file index: {}", list_index), + }); + }; + let cb_requested = min(BLOCK_SIZE as u64, file.size - self.progress.offset); + let conn_id = file.conn_id; + + let (n_position_high, n_position_low) = ( + (self.progress.offset >> 32) as i32, + (self.progress.offset & (u32::MAX as u64)) as i32, + ); + let request = ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags: 2, + n_position_low, + n_position_high, + cb_requested: cb_requested as _, + have_clip_data_id: false, + clip_data_id: 0, + }; + allow_err!(send_data(conn_id, request)); + self.progress.last_sent_time = Instant::now(); + + Ok(()) + } + + fn handle_file_contents_response( + &mut self, + file_contents: FileContentsResponse, + ) -> Result<(), CliprdrError> { + if let Some(file) = self.progress.file_handle.as_mut() { + let data = file_contents.requested_data.as_slice(); + let mut write_len = 0; + while write_len < data.len() { + match file.write(&data[write_len..]) { + Ok(len) => { + write_len += len; + } + Err(e) => { + return Err(CliprdrError::FileError { + path: self.progress.download_file_path.clone(), + err: e, + }); + } + } + } + self.update_next(write_len as _)?; + } else { + return Err(CliprdrError::FileError { + path: self.progress.download_file_path.clone(), + err: std::io::Error::new(std::io::ErrorKind::NotFound, "file handle is not opened"), + }); + } + Ok(()) + } +} diff --git a/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs b/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs new file mode 100644 index 00000000000..e55dd46b2bb --- /dev/null +++ b/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs @@ -0,0 +1,443 @@ +use super::{ + item_data_provider::create_pasteboard_file_url_provider, + paste_observer::PasteObserver, + paste_task::{FileContentsResponse, PasteTask}, +}; +use crate::{ + platform::unix::{ + filetype::FileDescription, FILECONTENTS_FORMAT_NAME, FILEDESCRIPTORW_FORMAT_NAME, + }, + send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ProgressPercent, +}; +use hbb_common::{allow_err, bail, log, ResultType}; +use objc2::{msg_send_id, rc::Id, runtime::ProtocolObject, ClassType}; +use objc2_app_kit::{NSPasteboard, NSPasteboardTypeFileURL}; +use objc2_foundation::{NSArray, NSString}; +use std::{ + io, + path::Path, + sync::{ + mpsc::{channel, Receiver, RecvTimeoutError, Sender}, + Arc, Mutex, + }, + thread, + time::Duration, +}; + +lazy_static::lazy_static! { + static ref PASTE_OBSERVER_INFO: Arc>> = Default::default(); +} + +pub const TEMP_FILE_PREFIX: &str = ".rustdesk_"; + +#[derive(Default, Debug, Clone, PartialEq)] +pub(super) struct PasteObserverInfo { + pub file_descriptor_id: i32, + pub conn_id: i32, + pub source_path: String, + pub target_path: String, +} + +impl PasteObserverInfo { + fn exit_msg() -> Self { + Self::default() + } +} + +struct ContextInfo { + tx: Sender>, + handle: thread::JoinHandle<()>, +} + +pub struct PasteboardContext { + pasteboard: Id, + observer: Arc>, + tx_handle: Option, + tx_remove_file: Option>, + remove_file_handle: Option>, + tx_paste_task: Sender, + paste_task: Arc>, +} + +unsafe impl Send for PasteboardContext {} +unsafe impl Sync for PasteboardContext {} + +impl Drop for PasteboardContext { + fn drop(&mut self) { + self.observer.lock().unwrap().stop(); + if let Some(tx_handle) = self.tx_handle.take() { + if tx_handle.tx.send(Ok(PasteObserverInfo::exit_msg())).is_ok() { + tx_handle.handle.join().ok(); + } + } + } +} + +impl CliprdrServiceContext for PasteboardContext { + fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { + Ok(()) + } + + fn empty_clipboard(&mut self, conn_id: i32) -> Result { + Ok(self.empty_clipboard_(conn_id)) + } + + fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { + self.server_clip_file_(conn_id, msg) + } + + fn get_progress_percent(&self) -> Option { + self.paste_task.lock().unwrap().progress_percent() + } + + fn cancel(&mut self) { + self.paste_task.lock().unwrap().cancel(); + } +} + +impl PasteboardContext { + fn init(&mut self) { + let (tx_remove_file, rx_remove_file) = channel(); + let handle_remove_file = Self::init_thread_remove_file(rx_remove_file); + self.tx_remove_file = Some(tx_remove_file.clone()); + self.remove_file_handle = Some(handle_remove_file); + + let (tx, rx) = channel(); + let observer: Arc> = self.observer.clone(); + let handle = Self::init_thread_observer(tx_remove_file, rx, observer); + self.tx_handle = Some(ContextInfo { tx, handle }); + } + + fn init_thread_observer( + tx_remove_file: Sender, + rx: Receiver>, + observer: Arc>, + ) -> thread::JoinHandle<()> { + let exit_msg = PasteObserverInfo::exit_msg(); + thread::spawn(move || loop { + match rx.recv() { + Ok(Ok(task_info)) => { + if task_info == exit_msg { + log::debug!("pasteboard item data provider: exit"); + break; + } + tx_remove_file.send(task_info.source_path.clone()).ok(); + observer.lock().unwrap().start(task_info); + } + Ok(Err(e)) => { + log::error!("pasteboard item data provider, inner error: {e}"); + } + Err(e) => { + log::error!("pasteboard item data provider, error: {e}"); + break; + } + } + }) + } + + fn init_thread_remove_file(rx: Receiver) -> thread::JoinHandle<()> { + thread::spawn(move || { + let mut cur_file: Option = None; + loop { + match rx.recv_timeout(Duration::from_secs(30)) { + Ok(path) => { + if let Some(file) = cur_file.take() { + if !file.is_empty() { + std::fs::remove_file(&file).ok(); + } + } + if !path.is_empty() { + cur_file = Some(path); + } + } + Err(e) => { + if let Some(file) = cur_file.take() { + if !file.is_empty() { + std::fs::remove_file(&file).ok(); + } + } + if e == RecvTimeoutError::Disconnected { + break; + } + } + } + } + }) + } + + // Just removing the file can also make paste option in the context menu disappear. + fn empty_clipboard_(&mut self, _conn_id: i32) -> bool { + self.tx_remove_file + .as_ref() + .map(|tx| tx.send("".to_string()).ok()); + true + } + + fn temp_files_count() -> usize { + let mut count = 0; + if let Ok(entries) = std::fs::read_dir("/tmp") { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_file() { + if let Some(file_name) = path.file_name() { + if let Some(file_name_str) = file_name.to_str() { + if file_name_str.starts_with(TEMP_FILE_PREFIX) { + count += 1; + } + } + } + } + } + } + } + count + } + + fn server_clip_file_(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { + match msg { + ClipboardFile::FormatList { format_list } => { + let temp_files = Self::temp_files_count(); + if temp_files >= 3 { + // The temp files should be 0 or 1 in normal case. + // We should not continue to paste files if there are more than 3 temp files. + return Err(CliprdrError::CommonError { + description: format!( + "too many temp files, current: {}, limit: {}", + temp_files, 3 + ), + }); + } + + let task_lock = self.paste_task.lock().unwrap(); + if !task_lock.is_finished() { + return Err(CliprdrError::CommonError { + description: "previous file paste task is not finished".to_string(), + }); + } + self.handle_format_list(conn_id, format_list)?; + } + ClipboardFile::FormatDataResponse { + msg_flags, + format_data, + } => { + self.handle_format_data_response(conn_id, msg_flags, format_data)?; + } + ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + requested_data, + } => { + self.handle_file_contents_response(conn_id, msg_flags, stream_id, requested_data)?; + } + ClipboardFile::TryEmpty => self.handle_try_empty(conn_id), + _ => {} + } + Ok(()) + } + + fn handle_format_list( + &self, + conn_id: i32, + format_list: Vec<(i32, String)>, + ) -> Result<(), CliprdrError> { + if let Some(tx_handle) = self.tx_handle.as_ref() { + if !format_list + .iter() + .find(|(_, name)| name == FILECONTENTS_FORMAT_NAME) + .map(|(id, _)| *id) + .is_some() + { + return Err(CliprdrError::CommonError { + description: "no file contents format found".to_string(), + }); + }; + let Some(file_descriptor_id) = format_list + .iter() + .find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME) + .map(|(id, _)| *id) + else { + return Err(CliprdrError::CommonError { + description: "no file descriptor format found".to_string(), + }); + }; + + let tx = tx_handle.tx.clone(); + let provider = create_pasteboard_file_url_provider( + PasteObserverInfo { + file_descriptor_id, + conn_id, + source_path: "".to_string(), + target_path: "".to_string(), + }, + tx, + ); + unsafe { + let types = NSArray::from_vec(vec![NSString::from_str( + &NSPasteboardTypeFileURL.to_string(), + )]); + let item = objc2_app_kit::NSPasteboardItem::new(); + item.setDataProvider_forTypes(&ProtocolObject::from_id(provider), &types); + self.pasteboard.clearContents(); + if !self + .pasteboard + .writeObjects(&Id::cast(NSArray::from_vec(vec![item]))) + { + return Err(CliprdrError::CommonError { + description: "failed to write objects".to_string(), + }); + } + } + } else { + return Err(CliprdrError::CommonError { + description: "pasteboard context is not inited".to_string(), + }); + } + Ok(()) + } + + fn handle_format_data_response( + &self, + conn_id: i32, + msg_flags: i32, + format_data: Vec, + ) -> Result<(), CliprdrError> { + log::debug!("handle format data response, msg_flags: {msg_flags}"); + if msg_flags != 0x1 { + // return failure message? + } + + let mut task_lock = self.paste_task.lock().unwrap(); + let target_dir = PASTE_OBSERVER_INFO + .lock() + .unwrap() + .as_ref() + .map(|task| task.target_path.clone()); + // unreachable in normal case + let Some(target_dir) = target_dir.as_ref().map(|d| Path::new(d).parent()).flatten() else { + return Err(CliprdrError::CommonError { + description: "failed to get parent path".to_string(), + }); + }; + // unreachable in normal case + if !target_dir.exists() { + return Err(CliprdrError::CommonError { + description: "target path does not exist".to_string(), + }); + } + let target_dir = target_dir.to_owned(); + match FileDescription::parse_file_descriptors(format_data, conn_id) { + Ok(files) => { + task_lock.start(target_dir, files); + Ok(()) + } + Err(e) => { + PASTE_OBSERVER_INFO + .lock() + .unwrap() + .replace(PasteObserverInfo::default()); + Err(e) + } + } + } + + fn handle_file_contents_response( + &self, + conn_id: i32, + msg_flags: i32, + stream_id: i32, + requested_data: Vec, + ) -> Result<(), CliprdrError> { + log::debug!("handle file contents response"); + self.tx_paste_task + .send(FileContentsResponse { + conn_id, + msg_flags, + stream_id, + requested_data, + }) + .ok(); + Ok(()) + } + + fn handle_try_empty(&mut self, conn_id: i32) { + log::debug!("empty_clipboard called"); + let ret = self.empty_clipboard_(conn_id); + log::debug!( + "empty_clipboard called, conn_id {}, return {}", + conn_id, + ret + ); + } +} + +fn handle_paste_result(task_info: &PasteObserverInfo) { + log::info!( + "file {} is pasted to {}", + &task_info.source_path, + &task_info.target_path + ); + if Path::new(&task_info.target_path).parent().is_none() { + log::error!( + "failed to get parent path of {}, no need to perform pasting", + &task_info.target_path + ); + return; + } + + PASTE_OBSERVER_INFO + .lock() + .unwrap() + .replace(task_info.clone()); + // to-do: add a timeout to clear data in `PASTE_OBSERVER_INFO`. + std::fs::remove_file(&task_info.source_path).ok(); + std::fs::remove_file(&task_info.target_path).ok(); + let data = ClipboardFile::FormatDataRequest { + requested_format_id: task_info.file_descriptor_id, + }; + allow_err!(send_data(task_info.conn_id as _, data)); +} + +#[inline] +pub fn create_pasteboard_context() -> ResultType> { + let pasteboard: Option> = + unsafe { msg_send_id![NSPasteboard::class(), generalPasteboard] }; + let Some(pasteboard) = pasteboard else { + bail!("failed to get general pasteboard"); + }; + let mut observer = PasteObserver::new(); + observer.init(handle_paste_result)?; + let (tx, rx) = channel(); + let mut context = Box::new(PasteboardContext { + pasteboard, + observer: Arc::new(Mutex::new(observer)), + tx_handle: None, + tx_remove_file: None, + remove_file_handle: None, + tx_paste_task: tx, + paste_task: Arc::new(Mutex::new(PasteTask::new(rx))), + }); + context.init(); + Ok(context) +} + +#[cfg(test)] +mod tests { + #[test] + fn test_temp_files_count() { + let mut c = super::PasteboardContext::temp_files_count(); + + for _ in 0..10 { + let path = format!( + "/tmp/{}{}", + super::TEMP_FILE_PREFIX, + uuid::Uuid::new_v4().to_string() + ); + if std::fs::File::create(&path).is_ok() { + c += 1; + } + } + + assert_eq!(c, super::PasteboardContext::temp_files_count()); + } +} diff --git a/libs/clipboard/src/platform/unix/mod.rs b/libs/clipboard/src/platform/unix/mod.rs index 7e7aeccb150..de5917f495b 100644 --- a/libs/clipboard/src/platform/unix/mod.rs +++ b/libs/clipboard/src/platform/unix/mod.rs @@ -2,9 +2,13 @@ use dashmap::DashMap; use lazy_static::lazy_static; mod filetype; +pub use filetype::{FileDescription, FileType}; /// use FUSE for file pasting on these platforms #[cfg(target_os = "linux")] pub mod fuse; +#[cfg(target_os = "macos")] +pub mod macos; + pub mod local_file; pub mod serv_files; diff --git a/libs/clipboard/src/platform/windows.rs b/libs/clipboard/src/platform/windows.rs index 1d8506eade8..3734406e0c9 100644 --- a/libs/clipboard/src/platform/windows.rs +++ b/libs/clipboard/src/platform/windows.rs @@ -6,8 +6,9 @@ #![allow(deref_nullptr)] use crate::{ - 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, + send_data, send_data_exclude, ClipboardFile, CliprdrError, CliprdrServiceContext, + ProgressPercent, ResultType, ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, + ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, }; use hbb_common::{allow_err, log}; use std::{ @@ -602,6 +603,12 @@ impl CliprdrServiceContext for CliprdrClientContext { let ret = server_clip_file(self, conn_id, msg); ret_to_result(ret) } + + fn get_progress_percent(&self) -> Option { + None + } + + fn cancel(&mut self) {} } fn ret_to_result(ret: u32) -> Result<(), CliprdrError> { @@ -745,7 +752,11 @@ pub fn server_clip_file( 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); + log::debug!( + "empty_clipboard called, conn_id {}, return {}", + conn_id, + ret + ); } } ret diff --git a/src/client.rs b/src/client.rs index 319b3f3da3e..d2ceffd3cca 100644 --- a/src/client.rs +++ b/src/client.rs @@ -848,6 +848,10 @@ impl ClientClipboardHandler { #[cfg(feature = "unix-file-copy-paste")] if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Client, false) { if !urls.is_empty() { + #[cfg(target_os = "macos")] + if crate::clipboard::is_file_url_set_by_rustdesk(&urls) { + return; + } if self.is_file_required() { match clipboard::platform::unix::serv_files::sync_files(&urls) { Ok(()) => { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 23d2f409420..448eac7f966 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -12,7 +12,10 @@ use crate::{ }; #[cfg(feature = "unix-file-copy-paste")] use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip}; -#[cfg(target_os = "windows")] +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] use clipboard::ContextSend; use crossbeam_queue::ArrayQueue; #[cfg(not(target_os = "ios"))] @@ -1956,9 +1959,9 @@ impl Remote { #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] async fn handle_cliprdr_msg( - &self, + &mut self, clip: hbb_common::message_proto::Cliprdr, - _peer: &mut Stream, + peer: &mut Stream, ) { log::debug!("handling cliprdr msg from server peer"); #[cfg(feature = "flutter")] @@ -1982,7 +1985,10 @@ impl Remote { "Process clipboard message from server peer, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}", stop, is_stopping_allowed, file_transfer_enabled); if !stop { - #[cfg(target_os = "windows")] + #[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") + ))] if let Err(e) = ContextSend::make_sure_enabled() { log::error!("failed to restart clipboard context: {}", e); }; @@ -1996,12 +2002,36 @@ impl Remote { } #[cfg(feature = "unix-file-copy-paste")] if crate::is_support_file_copy_paste_num(self.handler.lc.read().unwrap().version) { - if let Some(msg) = unix_file_clip::serve_clip_messages( - ClipboardSide::Client, - clip, - self.client_conn_id, - ) { - allow_err!(_peer.send(&msg).await); + let mut out_msg = None; + + #[cfg(target_os = "macos")] + if clipboard::platform::unix::macos::should_handle_msg(&clip) { + if let Err(e) = ContextSend::proc(|context| -> ResultType<()> { + context + .server_clip_file(self.client_conn_id, clip) + .map_err(|e| e.into()) + }) { + log::error!("failed to handle cliprdr msg: {}", e); + } + } else { + out_msg = unix_file_clip::serve_clip_messages( + ClipboardSide::Client, + clip, + self.client_conn_id, + ); + } + + #[cfg(not(target_os = "macos"))] + { + out_msg = unix_file_clip::serve_clip_messages( + ClipboardSide::Client, + clip, + self.client_conn_id, + ); + } + + if let Some(msg) = out_msg { + allow_err!(peer.send(&msg).await); } } } diff --git a/src/clipboard.rs b/src/clipboard.rs index 4ab4bc666df..ee976d68fd8 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -75,6 +75,24 @@ pub fn check_clipboard( None } +#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))] +pub fn is_file_url_set_by_rustdesk(url: &Vec) -> bool { + if url.len() != 1 { + return false; + } + url.iter() + .next() + .map(|s| { + for prefix in &["file:///tmp/.rustdesk_", "//tmp/.rustdesk_"] { + if s.starts_with(prefix) { + return s[prefix.len()..].parse::().is_ok(); + } + } + false + }) + .unwrap_or(false) +} + #[cfg(feature = "unix-file-copy-paste")] pub fn check_clipboard_files( ctx: &mut Option, @@ -110,7 +128,6 @@ pub fn update_clipboard_files(files: Vec, side: ClipboardSide) { #[cfg(feature = "unix-file-copy-paste")] pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) { - #[cfg(target_os = "linux")] std::thread::spawn(move || { let mut ctx = CLIPBOARD_CTX.lock().unwrap(); if ctx.is_none() { @@ -125,9 +142,22 @@ pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) { } } if let Some(mut ctx) = ctx.as_mut() { - use clipboard::platform::unix; - if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) { + #[cfg(target_os = "linux")] + { + use clipboard::platform::unix; + if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) { + ctx.try_empty_clipboard_files(_side); + } + } + #[cfg(target_os = "macos")] + { ctx.try_empty_clipboard_files(_side); + // No need to make sure the context is enabled. + clipboard::ContextSend::proc(|context| -> ResultType<()> { + context.empty_clipboard(_conn_id).ok(); + Ok(()) + }) + .ok(); } } }); @@ -351,27 +381,43 @@ impl ClipboardContext { Ok(()) } + #[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))] + fn get_file_urls_set_by_rustdesk( + data: Vec, + _side: ClipboardSide, + ) -> Vec { + for item in data.into_iter() { + if let ClipboardData::FileUrl(urls) = item { + if is_file_url_set_by_rustdesk(&urls) { + return urls; + } + } + } + vec![] + } + + #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] + fn get_file_urls_set_by_rustdesk(data: Vec, side: ClipboardSide) -> Vec { + let exclude_path = + clipboard::platform::unix::fuse::get_exclude_paths(side == ClipboardSide::Client); + data.into_iter() + .filter_map(|c| match c { + ClipboardData::FileUrl(urls) => Some( + urls.into_iter() + .filter(|s| s.starts_with(&*exclude_path)) + .collect::>(), + ), + _ => None, + }) + .flatten() + .collect::>() + } + #[cfg(feature = "unix-file-copy-paste")] fn try_empty_clipboard_files(&mut self, side: ClipboardSide) { let _lock = ARBOARD_MTX.lock().unwrap(); if let Ok(data) = self.get_formats(&[ClipboardFormat::FileUrl]) { - #[cfg(target_os = "linux")] - let exclude_path = - clipboard::platform::unix::fuse::get_exclude_paths(side == ClipboardSide::Client); - #[cfg(target_os = "macos")] - let exclude_path: Arc = Default::default(); - let urls = data - .into_iter() - .filter_map(|c| match c { - ClipboardData::FileUrl(urls) => Some( - urls.into_iter() - .filter(|s| s.starts_with(&*exclude_path)) - .collect::>(), - ), - _ => None, - }) - .flatten() - .collect::>(); + let urls = Self::get_file_urls_set_by_rustdesk(data, side); if !urls.is_empty() { // FIXME: // The host-side clear file clipboard `let _ = self.inner.clear();`, diff --git a/src/common.rs b/src/common.rs index dfd3fca44ac..23f7b4b0f0c 100644 --- a/src/common.rs +++ b/src/common.rs @@ -139,6 +139,10 @@ pub fn is_support_file_copy_paste_num(ver: i64) -> bool { ver >= hbb_common::get_version_number("1.3.8") } +pub fn is_support_file_paste_if_macos(ver: &str) -> bool { + hbb_common::get_version_number(ver) >= hbb_common::get_version_number("1.3.9") +} + // is server process, with "--server" args #[inline] pub fn is_server() -> bool { diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index a3cb65174cc..1d2f0a3fb56 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -115,6 +115,10 @@ impl Handler { fn check_clipboard_file(&mut self) { if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Host, false) { if !urls.is_empty() { + #[cfg(target_os = "macos")] + if crate::clipboard::is_file_url_set_by_rustdesk(&urls) { + return; + } match clipboard::platform::unix::serv_files::sync_files(&urls) { Ok(()) => { // Use `send_data()` here to reuse `handle_file_clip()` in `connection.rs`. diff --git a/src/server/connection.rs b/src/server/connection.rs index 4ac552a42ec..7b1173fd70e 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1267,11 +1267,13 @@ impl Connection { let is_unix_and_peer_supported = crate::is_support_file_copy_paste(&self.lr.version); #[cfg(not(feature = "unix-file-copy-paste"))] let is_unix_and_peer_supported = false; - // to-do: add file clipboard support for macos let is_both_macos = cfg!(target_os = "macos") && self.lr.my_platform == whoami::Platform::MacOS.to_string(); - let has_file_clipboard = - is_both_windows || (is_unix_and_peer_supported && !is_both_macos); + let is_peer_support_paste_if_macos = + crate::is_support_file_paste_if_macos(&self.lr.version); + let has_file_clipboard = is_both_windows + || (is_unix_and_peer_supported + && (!is_both_macos || is_peer_support_paste_if_macos)); platform_additions.insert("has_file_clipboard".into(), json!(has_file_clipboard)); } @@ -2195,11 +2197,38 @@ impl Connection { } #[cfg(feature = "unix-file-copy-paste")] if crate::is_support_file_copy_paste(&self.lr.version) { - if let Some(msg) = unix_file_clip::serve_clip_messages( - ClipboardSide::Host, - clip, - self.inner.id(), - ) { + let mut out_msg = None; + + #[cfg(target_os = "macos")] + if clipboard::platform::unix::macos::should_handle_msg(&clip) { + if let Err(e) = clipboard::ContextSend::make_sure_enabled() { + log::error!("failed to restart clipboard context: {}", e); + } else { + let _ = + clipboard::ContextSend::proc(|context| -> ResultType<()> { + context + .server_clip_file(self.inner.id(), clip) + .map_err(|e| e.into()) + }); + } + } else { + out_msg = unix_file_clip::serve_clip_messages( + ClipboardSide::Host, + clip, + self.inner.id(), + ); + } + + #[cfg(not(target_os = "macos"))] + { + out_msg = unix_file_clip::serve_clip_messages( + ClipboardSide::Host, + clip, + self.inner.id(), + ); + } + + if let Some(msg) = out_msg { self.send(msg).await; } } From 0d919157c912fe7d5c754742c579f0f1c82d5f2f Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 28 Feb 2025 11:56:17 +0800 Subject: [PATCH 118/506] Fix/win build (#10954) * fix: win build Signed-off-by: fufesou * fix: win, build Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 7a2b861c857..24a1897ad2d 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -525,8 +525,8 @@ packages: dependency: "direct main" description: path: "." - ref: "2ded7f146437a761ffe6981e2f742038f85ca68d" - resolved-ref: "2ded7f146437a761ffe6981e2f742038f85ca68d" + ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87" + resolved-ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87" 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 62d29b1b13f..a9aae014eff 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -94,7 +94,7 @@ dependencies: flutter_gpu_texture_renderer: git: url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer - ref: 2ded7f146437a761ffe6981e2f742038f85ca68d + ref: 08a471bb8ceccdd50483c81cdfa8b81b07b14b87 uuid: ^3.0.7 auto_size_text_field: ^2.2.1 flex_color_picker: ^3.3.0 From 41cd375e3c099a667cab30dc9f63781cdfba190f Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 28 Feb 2025 12:14:40 +0800 Subject: [PATCH 119/506] fix: potential memleak (#10955) Signed-off-by: fufesou --- .../platform/unix/macos/item_data_provider.rs | 2 +- .../platform/unix/macos/pasteboard_context.rs | 71 ++++++++++++------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/libs/clipboard/src/platform/unix/macos/item_data_provider.rs b/libs/clipboard/src/platform/unix/macos/item_data_provider.rs index b12e47d8013..95036312ed6 100644 --- a/libs/clipboard/src/platform/unix/macos/item_data_provider.rs +++ b/libs/clipboard/src/platform/unix/macos/item_data_provider.rs @@ -46,7 +46,7 @@ declare_class!( match std::fs::File::create(&path) { Ok(_) => { let url = format!("file:///{}", &path); - item.setString_forType(&NSString::from_str(&url), &NSPasteboardTypeFileURL); + item.setString_forType(&NSString::from_str(&url), &NSPasteboardTypeFileURL); let mut task_info = self.ivars().task_info.clone(); task_info.source_path = path; self.ivars().tx.send(Ok(task_info)).ok(); diff --git a/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs b/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs index e55dd46b2bb..4c747409363 100644 --- a/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs +++ b/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs @@ -10,7 +10,7 @@ use crate::{ send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ProgressPercent, }; use hbb_common::{allow_err, bail, log, ResultType}; -use objc2::{msg_send_id, rc::Id, runtime::ProtocolObject, ClassType}; +use objc2::{msg_send_id, rc::autoreleasepool, rc::Id, runtime::ProtocolObject, ClassType}; use objc2_app_kit::{NSPasteboard, NSPasteboardTypeFileURL}; use objc2_foundation::{NSArray, NSString}; use std::{ @@ -262,32 +262,7 @@ impl PasteboardContext { }); }; - let tx = tx_handle.tx.clone(); - let provider = create_pasteboard_file_url_provider( - PasteObserverInfo { - file_descriptor_id, - conn_id, - source_path: "".to_string(), - target_path: "".to_string(), - }, - tx, - ); - unsafe { - let types = NSArray::from_vec(vec![NSString::from_str( - &NSPasteboardTypeFileURL.to_string(), - )]); - let item = objc2_app_kit::NSPasteboardItem::new(); - item.setDataProvider_forTypes(&ProtocolObject::from_id(provider), &types); - self.pasteboard.clearContents(); - if !self - .pasteboard - .writeObjects(&Id::cast(NSArray::from_vec(vec![item]))) - { - return Err(CliprdrError::CommonError { - description: "failed to write objects".to_string(), - }); - } - } + autoreleasepool(|_| self.set_clipboard_item(tx_handle, conn_id, file_descriptor_id))?; } else { return Err(CliprdrError::CommonError { description: "pasteboard context is not inited".to_string(), @@ -296,6 +271,41 @@ impl PasteboardContext { Ok(()) } + fn set_clipboard_item( + &self, + tx_handle: &ContextInfo, + conn_id: i32, + file_descriptor_id: i32, + ) -> Result<(), CliprdrError> { + let tx = tx_handle.tx.clone(); + let provider = create_pasteboard_file_url_provider( + PasteObserverInfo { + file_descriptor_id, + conn_id, + source_path: "".to_string(), + target_path: "".to_string(), + }, + tx, + ); + unsafe { + let types = NSArray::from_vec(vec![NSString::from_str( + &NSPasteboardTypeFileURL.to_string(), + )]); + let item = objc2_app_kit::NSPasteboardItem::new(); + item.setDataProvider_forTypes(&ProtocolObject::from_id(provider), &types); + self.pasteboard.clearContents(); + if !self + .pasteboard + .writeObjects(&Id::cast(NSArray::from_vec(vec![item]))) + { + return Err(CliprdrError::CommonError { + description: "failed to write objects".to_string(), + }); + } + } + Ok(()) + } + fn handle_format_data_response( &self, conn_id: i32, @@ -427,6 +437,7 @@ mod tests { fn test_temp_files_count() { let mut c = super::PasteboardContext::temp_files_count(); + let mut created_files = vec![]; for _ in 0..10 { let path = format!( "/tmp/{}{}", @@ -434,10 +445,16 @@ mod tests { uuid::Uuid::new_v4().to_string() ); if std::fs::File::create(&path).is_ok() { + created_files.push(path); c += 1; } } assert_eq!(c, super::PasteboardContext::temp_files_count()); + + // Clean up the created files. + for file in created_files { + std::fs::remove_file(&file).ok(); + } } } From e17ab74040beec8409f29a96f17a5faeb72ed5e6 Mon Sep 17 00:00:00 2001 From: asereze Date: Sat, 1 Mar 2025 11:39:58 +0100 Subject: [PATCH 120/506] Sardinian translation (#10941) * Sardinian translation Sardinian ("Sardu", ISO 639-1 code: "sc") translation. * Update lang.rs * Corrected typo --- src/lang.rs | 3 + src/lang/sc.rs | 661 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 664 insertions(+) create mode 100644 src/lang/sc.rs diff --git a/src/lang.rs b/src/lang.rs index 70682267803..54f06d87499 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -33,6 +33,7 @@ mod pl; mod ptbr; mod ro; mod ru; +mod sc; mod sk; mod sl; mod sq; @@ -87,6 +88,7 @@ pub const LANGS: &[(&str, &str)] = &[ ("ar", "العربية"), ("he", "עברית"), ("hr", "Hrvatski"), + ("sc", "Sardu"), ]; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -161,6 +163,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "be" => be::T.deref(), "he" => he::T.deref(), "hr" => hr::T.deref(), + "sc" => sc::T.deref(), _ => en::T.deref(), }; let (name, placeholder_value) = extract_placeholder(&name); diff --git a/src/lang/sc.rs b/src/lang/sc.rs new file mode 100644 index 00000000000..4953fe85b38 --- /dev/null +++ b/src/lang/sc.rs @@ -0,0 +1,661 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Istadu"), + ("Your Desktop", "Custu elaboradore"), + ("desk_tip", "Podes atzèdere a custu elaboradore impreende s'ID e sa crae de intrada inditados inoghe in suta."), + ("Password", "Crae"), + ("Ready", "Prontu"), + ("Established", "Istabilida"), + ("connecting_status", "Connessione a sa rete RustDesk..."), + ("Enable service", "Abìlita servìtziu"), + ("Start service", "Avia su servìtziu"), + ("Service is running", "Su servìtziu est in esecutzione"), + ("Service is not running", "Su servìtziu no est in esecutzione"), + ("not_ready_status", "Non prontu. Verìfica sa connessione"), + ("Control Remote Desktop", "Controlla s'elaboradore remotu"), + ("Transfer file", "Tràmuda documentos"), + ("Connect", "Cunnete·ti"), + ("Recent sessions", "Sessiones reghentes"), + ("Address book", "Rubrica"), + ("Confirmation", "Cunfirma"), + ("TCP tunneling", "Tunnel TCP"), + ("Remove", "Boga"), + ("Refresh random password", "Crae casuale noa"), + ("Set your own password", "Imposta sa crae"), + ("Enable keyboard/mouse", "Abìlita tecladu/ratu"), + ("Enable clipboard", "Abìlita punta de billete"), + ("Enable file transfer", "Abìlita su tramudòngiu de documentos"), + ("Enable TCP tunneling", "Abìlita tunnel TCP"), + ("IP Whitelisting", "IP autorizados"), + ("ID/Relay Server", "Serbidore ID/Tràmuda"), + ("Import server config", "Importa configuratzione serbidore dae sa punta de billete"), + ("Export Server Config", "Esporta configurazione serbidore a sa punta de billete"), + ("Import server configuration successfully", "Configuratzione serbidore importada cumprida"), + ("Export server configuration successfully", "Configuratzione serbidore esportada cumprida"), + ("Invalid server configuration", "Configuratzione serbidore non vàlida"), + ("Clipboard is empty", "Sa punta de billete est bòida"), + ("Stop service", "Firma su servìtziu"), + ("Change ID", "Càmbia ID"), + ("Your new ID", "S'ID nou"), + ("length %min% to %max%", "longària dae %min% a %max%"), + ("starts with a letter", "incumintza cun una lìtera"), + ("allowed characters", "caràteres cunsentidos"), + ("id_change_tip", "Podes impreare petzi sos caràteres a-z, A-Z, 0-9, - (tratigheddu) e _ (sutaliniadu).\nSu primu caràtere depet èssere a-z o A-Z.\nSa longària depet èssere de intre 6 e 16 caràteres."), + ("Website", "Situ web programma"), + ("About", "Info programma"), + ("Slogan_tip", "Fatu cun su coro in custu mundu caòticu!"), + ("Privacy Statement", "Informativa subra de sa riservadesa"), + ("Mute", "Sonu istudadu"), + ("Build Date", "Data build"), + ("Version", "Versione"), + ("Home", "Pàgina printzipale"), + ("Audio Input", "Intrada àudio"), + ("Enhancements", "Megioros"), + ("Hardware Codec", "Codificadore fìsicu (hardware)"), + ("Adaptive bitrate", "Velotzidade de bits adativa"), + ("ID Server", "ID serbidore"), + ("Relay Server", "Serbidore de tràmuda"), + ("API Server", "Serbidore API"), + ("invalid_http", "depet incumintzare cun http:// o https://"), + ("Invalid IP", "Indiritzu IP non vàlidu"), + ("Invalid format", "Formadu non vàlidu"), + ("server_not_support", "Galu non suportadu dae su serbidore"), + ("Not available", "No a disponimentu"), + ("Too frequent", "Tropu fitianu"), + ("Cancel", "Annulla"), + ("Skip", "Ignora"), + ("Close", "Serra"), + ("Retry", "Torra a proare"), + ("OK", "AB"), + ("Password Required", "Bisòngiat sa crae"), + ("Please enter your password", "Inserta sa crae tua"), + ("Remember password", "Ammenta sa crae"), + ("Wrong Password", "Crae isballiada"), + ("Do you want to enter again?", "Boles torrare a intrare?"), + ("Connection Error", "Errore de connessione"), + ("Error", "Errore"), + ("Reset by the peer", "Resetada dae su dispositivu de s'àtera parte"), + ("Connecting...", "Connetende..."), + ("Connection in progress. Please wait.", "Connetende. Iseta."), + ("Please try 1 minute later", "Torra a proare a pustis de 1 minutu"), + ("Login Error", "Faddina de atzessu"), + ("Successful", "Cumpridu"), + ("Connected, waiting for image...", "Connessu, isetende s'immàgine..."), + ("Name", "Nùmene"), + ("Type", "Casta"), + ("Modified", "Modificadu"), + ("Size", "Mannària"), + ("Show Hidden Files", "Mustra sos documentos cuados"), + ("Receive", "Retzi"), + ("Send", "Imbia"), + ("Refresh File", "Annoa sos documentos"), + ("Local", "Locale"), + ("Remote", "Remotu"), + ("Remote Computer", "Elaboradore remotu"), + ("Local Computer", "Elaboradore locale"), + ("Confirm Delete", "Cunfirma s'iscantzelladura"), + ("Delete", "Iscantzella"), + ("Properties", "Propiedades"), + ("Multi Select", "Seletzione mùltipla"), + ("Select All", "Seletziona totu"), + ("Unselect All", "Deseletziona totu"), + ("Empty Directory", "Cartella bòida"), + ("Not an empty directory", "No est una cartella bòida"), + ("Are you sure you want to delete this file?", "Ses seguru de bòlere iscantzellare custu documentu?"), + ("Are you sure you want to delete this empty directory?", "Ses seguru de bòlere iscantzellare custa cartella bòida?"), + ("Are you sure you want to delete the file of this directory?", "Ses seguru de bòlere iscantzellare su documentu de custa cartella?"), + ("Do this for all conflicts", "Ammenta custu issèberu pro totu sos cunflitos"), + ("This is irreversible!", "Custu non si podet annullare!"), + ("Deleting", "Iscantzellende"), + ("files", "documentos"), + ("Waiting", "Isetende"), + ("Finished", "Acabadu"), + ("Speed", "Lestresa"), + ("Custom Image Quality", "Calidade immàgine personalizada"), + ("Privacy mode", "Modalidade de riservadesa"), + ("Block user input", "Bloca sas atziones de utente"), + ("Unblock user input", "Isbloca sas atziones de utente"), + ("Adjust Window", "Adata sa ventana"), + ("Original", "Originale"), + ("Shrink", "Astringhe"), + ("Stretch", "Illàrghia"), + ("Scrollbar", "Istanga de iscurrimentu"), + ("ScrollAuto", "Iscurre in automàticu"), + ("Good image quality", "Calidade bona de s'immàgine"), + ("Balanced", "Bilantziada"), + ("Optimize reaction time", "Otimiza su tempus de reatzione"), + ("Custom", "Profilu personalizadu"), + ("Show remote cursor", "Mustra su cursore remotu"), + ("Show quality monitor", "Mustra sa calidade vìdeu"), + ("Disable clipboard", "Disabìlita sa punta de billete"), + ("Lock after session end", "Bloca a sa fine de sa sessione"), + ("Insert Ctrl + Alt + Del", "Inserta Ctrl + Alt + Del"), + ("Insert Lock", "Blocu insertada"), + ("Refresh", "Annoa"), + ("ID does not exist", "S'ID no esistit"), + ("Failed to connect to rendezvous server", "Errore connessione a su sebidore de atòbiu"), + ("Please try later", "Torra a proare prus a a tardu"), + ("Remote desktop is offline", "S'iscrivania remota no est in lìnia"), + ("Key mismatch", "Sa crae non currispondet"), + ("Timeout", "Tempus iscadidu"), + ("Failed to connect to relay server", "Connessione a su serbidore de tràmuda fallida"), + ("Failed to connect via rendezvous server", "Connessione pro mèdiu de su serbidore de atòbiu fallida"), + ("Failed to connect via relay server", "Connessione pro mèdiu de su serbidore de tràmuda fallida"), + ("Failed to make direct connection to remote desktop", "Connessione direta a s'iscrivania remota fallida"), + ("Set Password", "Imposta sa crae"), + ("OS Password", "Crae sistema operativu"), + ("install_tip", "Pro neghe de su Controllu Contu Utente (UAC), RustDesk diat pòdere non funtzionare comente si tocat comente iscrivania remota.\nPro evitare custu problema, incarca in su butone inoghe in suta pro installare RustDesk a livellu de sistema."), + ("Click to upgrade", "Incarca pro atualizare"), + ("Click to download", "Iscàrriga"), + ("Click to update", "Incarca pro annoare"), + ("Configure", "Cunfigura"), + ("config_acc", "Pro controllare s'iscrivania dae foras, depes frunire a RustDesk su permissu 'Atzessibilidade'."), + ("config_screen", "Pro controllare s'iscrivania dae foras, depes frunire a RustDesk su permissu 'Registratzione ischermu'."), + ("Installing ...", "Installatzione ..."), + ("Install", "Installa"), + ("Installation", "Installatzione"), + ("Installation Path", "Àndala de installatzione"), + ("Create start menu shortcuts", "Crea sos ligàmenes in su menù de incumintzu"), + ("Create desktop icon", "Crea un'icona in s'iscrivania"), + ("agreement_tip", "Incaminende s'installazione, atzetas sos tèrmines de su cuntratu de lissèntzia."), + ("Accept and Install", "Atzeta e installa"), + ("End-user license agreement", "Cuntratu de lissèntzia utente finale"), + ("Generating ...", "Ingendrende ..."), + ("Your installation is lower version.", "Cuta installazione no est atualizada."), + ("not_close_tcp_tip", "Non Serres custa ventana in su mentres chi ses impreende su tunnel"), + ("Listening ...", "Ascurtende ..."), + ("Remote Host", "Istrangiaore (host) remotu"), + ("Remote Port", "Ghenna remota"), + ("Action", "Atzione"), + ("Add", "Annanghe"), + ("Local Port", "Ghenna locale"), + ("Local Address", "Indiritzu locale"), + ("Change Local Port", "Càmbia ghenna locale"), + ("setup_server_tip", "Pro una connessione prus lestra, cunfigura unu serbidore ispetzìficu"), + ("Too short, at least 6 characters.", "Tropu curtza, a su nessi 6 caràteres"), + ("The confirmation is not identical.", "Sa crae de cunfirma non currispondet"), + ("Permissions", "Permissos"), + ("Accept", "Atzeta"), + ("Dismiss", "Naga"), + ("Disconnect", "Iscollega·ti"), + ("Enable file copy and paste", "Permite sa còpia e s'incollòngiu de documentos"), + ("Connected", "Connessu"), + ("Direct and encrypted connection", "Connessione direta e tzifrada"), + ("Relayed and encrypted connection", "Connessione inoltrada (relayed) e tzifrada"), + ("Direct and unencrypted connection", "Connessione direta e non tzifrada"), + ("Relayed and unencrypted connection", "Connessione inoltrada (relayed) e non tzifrada"), + ("Enter Remote ID", "Inserta ID remotu"), + ("Enter your password", "Inserta sa crae tua"), + ("Logging in...", "Intrende..."), + ("Enable RDP session sharing", "Abìlita sa cumpartzidura sessione RDP"), + ("Auto Login", "Atzessu automàticu"), + ("Enable direct IP access", "Abìlita s'intrada direta pro mèdiu de s'IP"), + ("Rename", "Càmbia de nùmene"), + ("Space", "Ispàtziu"), + ("Create desktop shortcut", "Crea unu ligàmene in s'iscrivania"), + ("Change Path", "Modìfica s'àndala"), + ("Create Folder", "Crea una cartella"), + ("Please enter the folder name", "Inserta su nùmene de sa cartella"), + ("Fix it", "Risolve"), + ("Warning", "Avisu"), + ("Login screen using Wayland is not supported", "S'ischemada de intrada no est suportada impreende Wayland"), + ("Reboot required", "B'at bisòngiu de una torrada a aviare"), + ("Unsupported display server", "Serbidore de visualizatzione non suportadu"), + ("x11 expected", "bisòngiat xll"), + ("Port", "Ghenna"), + ("Settings", "Impostatziones"), + ("Username", "Nùmene utente"), + ("Invalid port", "Nùmeru ghenna non vàlidu"), + ("Closed manually by the peer", "Serradu a manu dae su dispositivu remotu"), + ("Enable remote configuration modification", "Abìlita sa modìfica remota de sa cunfiguratzione"), + ("Run without install", "Allughe chene installare"), + ("Connect via relay", "Collega·ti impreende una tràmuda relay"), + ("Always connect via relay", "Collega·ti semper impreende una tràmuda relay"), + ("whitelist_tip", "Si podent connètere a custa iscrivania petzi sos indiritzos IP autorizados"), + ("Login", "Intra"), + ("Verify", "Avèrgua"), + ("Remember me", "Ammenta·ti de mene"), + ("Trust this device", "Registra custu dispositivu comente de fidùtzia"), + ("Verification code", "Còdighe de verìfica"), + ("verification_tip", "Amus imbiadu unu còdighe de averguada a s'indiritzu de posta eletrònica registradu, pro intrare inserta·lu."), + ("Logout", "Essi"), + ("Tags", "Etichetas"), + ("Search ID", "Chirca ID"), + ("whitelist_sep", "Separados dae vìrgulas, puntu e vìrgula, ispatziu o riga a suta"), + ("Add ID", "Annanghe ID"), + ("Add Tag", "Annanghe eticheta"), + ("Unselect all tags", "Deseletziona totu sas etichetas"), + ("Network error", "Errore de rete"), + ("Username missed", "Mancat su nùmene utente"), + ("Password missed", "Mancat sa crae de intrada"), + ("Wrong credentials", "Credentziales isballiadas"), + ("The verification code is incorrect or has expired", "Su còdighe de verìfica no est curretu o est iscadidu"), + ("Edit Tag", "Modìfica eticheta"), + ("Forget Password", "Ismèntiga sa crae"), + ("Favorites", "Preferidos"), + ("Add to Favorites", "Annanghe a sos preferidos"), + ("Remove from Favorites", "Boga dae sos preferidos"), + ("Empty", "Bòidu"), + ("Invalid folder name", "Nùmene de sa cartella non vàlidu"), + ("Socks5 Proxy", "Serbidore intermediàriu Socks5"), + ("Socks5/Http(s) Proxy", "Serbidore intermediàriu Socks5/Http(s)"), + ("Discovered", "Rileva"), + ("install_daemon_tip", "Pro aviare su programma a s'allughìngiu, tocat a l'installare comente servìtziu de sistema."), + ("Remote ID", "ID remotu"), + ("Paste", "Incolla"), + ("Paste here?", "Incollare inoghe?"), + ("Are you sure to close the connection?", "Ses seguru de bòlere serrare sa connessione?"), + ("Download new version", "Iscàrriga sa versione noa"), + ("Touch mode", "Modalidade tocu"), + ("Mouse mode", "Modalidade ratu"), + ("One-Finger Tap", "Tocu cun unu pòddighe"), + ("Left Mouse", "Butone de manca de su ratu"), + ("One-Long Tap", "Tocu longu cun unu pòddighe"), + ("Two-Finger Tap", "Tocu cun duos pòddighes"), + ("Right Mouse", "Butone de destra de su ratu"), + ("One-Finger Move", "Movimentu cun unu pòddighe"), + ("Double Tap & Move", "Tocu dòpiu e movimentu"), + ("Mouse Drag", "Trisinada de su ratu"), + ("Three-Finger vertically", "Tres pòddighes in verticale"), + ("Mouse Wheel", "Rodedda de su ratu"), + ("Two-Finger Move", "Movimentu cun duos pòddighes"), + ("Canvas Move", "Isposta sa tela"), + ("Pinch to Zoom", "Pìtziga pro ismanniare"), + ("Canvas Zoom", "Ismanniamentu tela"), + ("Reset canvas", "Reseta sa tela"), + ("No permission of file transfer", "Perunu permissu pro sa tràmuda de documentos"), + ("Note", "Nota"), + ("Connection", "Connessione"), + ("Share Screen", "Cumpartzi ischermu"), + ("Chat", "Tzarrada"), + ("Total", "Totale"), + ("items", "Elementos"), + ("Selected", "Seletzionadu"), + ("Screen Capture", "Catura de ischermu"), + ("Input Control", "Controllu atziones"), + ("Audio Capture", "Catura de s'àudio"), + ("File Connection", "Connessione documentos"), + ("Screen Connection", "Connessione ischermu"), + ("Do you accept?", "Atzetas?"), + ("Open System Setting", "Aberi sas impostatziones de sistema"), + ("How to get Android input permission?", "Comente otènnere s'autorizatzione de intrada (input) in Android?"), + ("android_input_permission_tip1", "Pro chi unu dispositivu remotu potzat controllare unu dispositivu Android pro mèdiu de unu ratu o cun su tocu, depes cunsentire a RustDesk de impreare su servìtziu 'Atzessibilidade'."), + ("android_input_permission_tip2", "Bae a sa pàgina de sas impostatziones de sistema chi s'at a abèrrere a pustis, busca e intra a [Servìtzios installados], allughe su servìtziu [Intrada RustDesk]."), + ("android_new_connection_tip", "Est istada retzida una dimanda noa de controllu pro su dispositivu atuale."), + ("android_service_will_start_tip", "S'ativatzione de Catura ischermu at a aviare in automàticu su servìtziu, permitende a àteros dispositivos de pedire una connessione dae custu dispositivu."), + ("android_stop_service_tip", "Sa serrada de su servìtziu at a tancare in automàticu totu sas connessiones istabilidas."), + ("android_version_audio_tip", "Sa versione atuale de Android non suportat s'achirimentu àudio, faghe s'atualizatzione a Android 10 o versiones prus noas."), + ("android_start_service_tip", "Pro aviare su servìtziu de cumpartzidura de s'ischermu seletziona [Avia su servìtziu] o abìlita s'autorizatzione [Catura de ischermu]."), + ("android_permission_may_not_change_tip", "Sas autorizatziones pro sas connessiones istabilidas non si podent modificare in manera istantànea finas a sa riconnessione."), + ("Account", "Contu"), + ("Overwrite", "Subraiscrie"), + ("This file exists, skip or overwrite this file?", "Custu documentu esistit, boles ignorare o subraiscìere custu archìviu?"), + ("Quit", "Essi"), + ("Help", "Agiudu"), + ("Failed", "Fallidu"), + ("Succeeded", "Cumpridu"), + ("Someone turns on privacy mode, exit", "Calicunu at allutu sa modalidade de riservadesa, essida"), + ("Unsupported", "Non suportadu"), + ("Peer denied", "Atzessu negadu a su dispositivu remotu"), + ("Please install plugins", "Installa sos cumplementos"), + ("Peer exit", "Essida dae su dispostivu remotu"), + ("Failed to turn off", "Non faghet a istudare"), + ("Turned off", "Istuda"), + ("Language", "Limba"), + ("Keep RustDesk background service", "Mantene su servìtziu de RustDesk in s'isfundu"), + ("Ignore Battery Optimizations", "Ignora sas otimizatziones de sa bateria"), + ("android_open_battery_optimizations_tip", "Si boles disabilitare custa funtzione, bae a sas impostatziones de s'aplicatzione RustDesk, aberi sa setzione 'Bateria' e boga sa seletzione a 'Chene restritziones'."), + ("Start on boot", "Avia a s'allughidura"), + ("Start the screen sharing service on boot, requires special permissions", "S'aviu de su servìtziu de cumpartzidura de s'ischermu a s'allughidura tenet bisòngiu de permissos ispetziales"), + ("Connection not allowed", "Connessione non permìtida"), + ("Legacy mode", "Modalidade antiga"), + ("Map mode", "Modalidade mapa"), + ("Translate mode", "Modalidade tradutzione"), + ("Use permanent password", "Imprea una crae de intrada permanente"), + ("Use both passwords", "Imprea craes de intrada monoimpreu e permanente"), + ("Set permanent password", "Imposta sa crae permanente"), + ("Enable remote restart", "Abìlita riaviu dae remotu"), + ("Restart remote device", "Torra a aviare su dispositivu remotu"), + ("Are you sure you want to restart", "Ses seguru de bòlere torrare a allùghere?"), + ("Restarting remote device", "Su dispositivu remotu s'est torrende a allùghere"), + ("remote_restarting_tip", "Torra a allùghere su dispositivu remotu"), + ("Copied", "Copiadu"), + ("Exit Fullscreen", "Essi dae sa modalidade a ischermu intreu"), + ("Fullscreen", "A ischermu intreu"), + ("Mobile Actions", "Atziones mòbiles"), + ("Select Monitor", "Seleziona ischermu"), + ("Control Actions", "Atziones de controllu"), + ("Display Settings", "Impostatziones de visualizatzione"), + ("Ratio", "Raportu"), + ("Image Quality", "Calidade de s'immàgine"), + ("Scroll Style", "Istile de iscurrimentu"), + ("Show Toolbar", "Mustra s'istanga de trastes"), + ("Hide Toolbar", "Cua s'istanga de trastes"), + ("Direct Connection", "Connessione direta"), + ("Relay Connection", "Connessione tramudada (relay)"), + ("Secure Connection", "Connessione segura"), + ("Insecure Connection", "Connessione non segura"), + ("Scale original", "Iscala originale"), + ("Scale adaptive", "Iscala adativa"), + ("General", "Generale"), + ("Security", "Seguresa"), + ("Theme", "Tema"), + ("Dark Theme", "Tema iscuru"), + ("Light Theme", "Tema craru"), + ("Dark", "Iscuru"), + ("Light", "Craru"), + ("Follow System", "Sistema"), + ("Enable hardware codec", "Abìlita codificadore fìsicu"), + ("Unlock Security Settings", "Isbloca sas impostatziones de seguresa"), + ("Enable audio", "Abìlita àudio"), + ("Unlock Network Settings", "Isbloca impostatziones de rete"), + ("Server", "Serbidore"), + ("Direct IP Access", "Atzessu IP diretu"), + ("Proxy", "Serbidore intermediàriu"), + ("Apply", "Àplica"), + ("Disconnect all devices?", "Boles iscollegare totu sos dispositivos?"), + ("Clear", "Isbòida"), + ("Audio Input Device", "Dispositivu intrada àudio"), + ("Use IP Whitelisting", "Imprea elencu IP autorizados"), + ("Network", "Rete"), + ("Pin Toolbar", "Bloca s'istanga de trastes"), + ("Unpin Toolbar", "Isbloca s'istanga de trastes"), + ("Recording", "Registratzione"), + ("Directory", "Cartella"), + ("Automatically record incoming sessions", "Registra in automàticu sas sessiones in intrada"), + ("Automatically record outgoing sessions", "Registra in automàticu sas sessiones in essida"), + ("Change", "Modìfica"), + ("Start session recording", "Incumintza sa registrazione de sa sessione"), + ("Stop session recording", "Firma sa registrazione de sa sessione"), + ("Enable recording session", "Abìlita sa registrazione de sa sessione"), + ("Enable LAN discovery", "Abìlita su rilevamentu LAN"), + ("Deny LAN discovery", "Disabìlita su rilevamentu LAN"), + ("Write a message", "Iscrie unu messàgiu"), + ("Prompt", "Pedi"), + ("Please wait for confirmation of UAC...", "Iseta sa cunfirma de s'UAC..."), + ("elevated_foreground_window_tip", "Sa ventana atuale de s'elaboradore remotu tenet bisòngiu, pro funtzionare, de privilègios prus mannos, duncas non faghet a impreare in manera temporànea su ratu e su tecladu.\nSi podet pedire a s'utente remotu de minimare a icona sa ventana atuale o de seletzionare su pulsante de artària in sa ventana de gestione de sa connessione.\nPro evitare custu problema, ti cussigiamus de installare su programma in su dispositivu remotu."), + ("Disconnected", "Iscollegadu"), + ("Other", "Àteru"), + ("Confirm before closing multiple tabs", "Cunfirma in antis de serrare prus ischedas"), + ("Keyboard Settings", "Impostatziones de tecladu"), + ("Full Access", "Atzessu cumpridu"), + ("Screen Share", "Cumpartzidura de ischermu"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland tenet bisòngiu de Ubuntu 21.04 o versione prus noa."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland tenet bisòngiu de una versione prus noa de sa distributzione Linux.\nProa X11 pro elaboradores o càmbia su sistema operativu."), + ("JumpLink", "Bae a"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seletziona s'ischermu de cumpartzire (òpera dae s'ala de su dispositivu remotu)."), + ("Show RustDesk", "Mustra RustDesk"), + ("This PC", "Custu PC"), + ("or", "O"), + ("Continue with", "Sighi cun"), + ("Elevate", "Cresche"), + ("Zoom cursor", "Cursore de ismanniamentu"), + ("Accept sessions via password", "Atzeta sessiones cun sa crae"), + ("Accept sessions via click", "Atzeta sessiones cun sas incarcadas"), + ("Accept sessions via both", "Atzeta sessiones cun totu sas duas craes"), + ("Please wait for the remote side to accept your session request...", "Iseta chi su dispositivu remotu atzetet sa dimanda de sessione..."), + ("One-time Password", "Crae monoimpreu"), + ("Use one-time password", "Imprea crae monoimpreu"), + ("One-time password length", "Longària crae monoimpreu"), + ("Request access to your device", "Pedi s'atzessu a su dispositivu"), + ("Hide connection management window", "Cua sa ventana de gestione de sas connessiones"), + ("hide_cm_tip", "Permite de cuare petzi si s'atzetant sessiones cun crae permanente"), + ("wayland_experiment_tip", "Su suportu Wayland est in fase isperimentale, si boles un'atzessu istàbile imprea X11."), + ("Right click to select tabs", "Incarca cun su pulsante destru pro seletzionare sas ischedas"), + ("Skipped", "Brincadu"), + ("Add to address book", "Annanghe a sa rubrica"), + ("Group", "Grupu"), + ("Search", "Chirca"), + ("Closed manually by web console", "Serra in manera manuale dae sa console web"), + ("Local keyboard type", "Casta de tecladu locale"), + ("Select local keyboard type", "Seletziona sa casta de tecladu locale"), + ("software_render_tip", "Si in s'elaboradore cun Linux b'at un'ischeda grafica Nvidia e sa ventana remota si serrat deretu a pustis de sa connessione, installa su driver nou a còdighe abertu e imprea sa renderitzatzione tràmite programma (software).\nDiat pòdere bisongiare a torrare a allùghere su programma."), + ("Always use software rendering", "Imprea semper sa renderizatzione tràmite programma"), + ("config_input", "Pro controllare s'elaboradore remotu cun su tecladu bisòngiat a frunire a RustDesk sos permissos de 'Monitoràgiu insertada'."), + ("config_microphone", "Per pòdere mutire, bisòngiat a frunire su premissu 'Registra àudio' a RustDesk."), + ("request_elevation_tip", "Si b'at calicunu in s'ala remota si podet pedire sa crèschida."), + ("Wait", "Iseta"), + ("Elevation Error", "Faddina durante sa crèschida de sos deretos"), + ("Ask the remote user for authentication", "Pedi s'autenticatzione a s'utente remotu"), + ("Choose this if the remote account is administrator", "Issèbera custa optzione si su contu remotu est amministradore"), + ("Transmit the username and password of administrator", "Trasmite su nùmene utente e sa crae de intrada de s'amministradore"), + ("still_click_uac_tip", "Torra a pedire chi s'utente remotu seletziones 'AB' in sa ventana UAC de s'esecutzione de RustDesk."), + ("Request Elevation", "Pedi sa crèschida de sos deretos"), + ("wait_accept_uac_tip", "Iseta chi s'utente remotu atzetet sa ventana de diàlogu UAC."), + ("Elevate successfully", "Crèschida de sos deretos cumprida"), + ("uppercase", "Majùscula"), + ("lowercase", "Minùscula"), + ("digit", "Nùmeru"), + ("special character", "Caràtere ispetziale"), + ("length>=8", "Lunghezza >= 8"), + ("Weak", "Dèbile"), + ("Medium", "Mesana"), + ("Strong", "Forte"), + ("Switch Sides", "Càmbia ala"), + ("Please confirm if you want to share your desktop?", "Boles cumpartzire s'elaboradore?"), + ("Display", "Visualizatzione"), + ("Default View Style", "Istile de visualiztazione predefinidu"), + ("Default Scroll Style", "Istile de iscurrimentu predefinidu"), + ("Default Image Quality", "Calidade de s'immàgine predefinida"), + ("Default Codec", "Codificadore predefinidu"), + ("Bitrate", "Tassu de bits"), + ("FPS", "FPS"), + ("Auto", "Automàticu"), + ("Other Default Options", "Àteras optziones predefinidas"), + ("Voice call", "Mutida vocale"), + ("Text chat", "Tzarrada de testu"), + ("Stop voice call", "Interrumpe sa mutida vocale"), + ("relay_hint_tip", "Si non faghet a si connètere in manera direta, podes proare a ti collegare impreende unu serbidore de tràmuda.\nIn prus, si boles imprearevsu serbidore de tràmuda in su primu tentativu, podes annànghere a s'ID su suffissu '/r\' o seletzionare in s'ischeda si esistit s'optzione 'Collega·ti semper impreende una tràmuda relay'."), + ("Reconnect", "Collega·ti torra"), + ("Codec", "Codificadore"), + ("Resolution", "Risolutzione"), + ("No transfers in progress", "Peruna tràmuda in cursu"), + ("Set one-time password length", "Imposta sa longària de sa crae monoimpreu"), + ("RDP Settings", "Impostatziones RDP"), + ("Sort by", "Òrdina pro"), + ("New Connection", "Connessione noa"), + ("Restore", "Riprìstina"), + ("Minimize", "Mìnima"), + ("Maximize", "Massimiza"), + ("Your Device", "Custu dispositivu"), + ("empty_recent_tip", "Non b'at galu peruna sessione reghente!\nPianifica·nde una."), + ("empty_favorite_tip", "Galu peruna connessione?\nBusca calicunu cun chie ti collegare e annanghe·lu a sos preferidos!"), + ("empty_lan_tip", "Paret a beru chi non siat istada atzapada peruna connessione."), + ("empty_address_book_tip", "Paret chi pro como in sa rubrica non b'apat connessiones."), + ("eg: admin", "es: admin"), + ("Empty Username", "Nùmene utente bòidu"), + ("Empty Password", "Crae bòida"), + ("Me", "Deo"), + ("identical_file_tip", "Custu archìviu est pretzisu a su chi b'at in su dispositivu remotu."), + ("show_monitors_tip", "Mustra sos ischermos in s'istanga de sos trastes"), + ("View Mode", "Modalidade de visualizatzione"), + ("login_linux_tip", "Intra a su contu de Linux remotu"), + ("verify_rustdesk_password_tip", "Cunfirma sa crae de RustDesk"), + ("remember_account_tip", "Ammenta custu contu"), + ("os_account_desk_tip", "Custu contu s'impreat pro intrare a su sistema operativu remotu e ativare sa sessione de s'elaboradore in modalidade non presidiada."), + ("OS Account", "Contu sistema operativu"), + ("another_user_login_title_tip", "Un'àteru utente at giai fatu s'atzessu."), + ("another_user_login_text_tip", "Separadu"), + ("xorg_not_found_title_tip", "Xorg no atzapadu."), + ("xorg_not_found_text_tip", "Installa Xorg."), + ("no_desktop_title_tip", "Non b'at perunu ambiente de elaboradore a disponimentu."), + ("no_desktop_text_tip", "Installa s'ambiente de elaboradore GNOME."), + ("No need to elevate", "Crèschida de sos privilègios non pedida"), + ("System Sound", "Dispositivu àudio de sistema"), + ("Default", "Predefinida"), + ("New RDP", "RDP nou"), + ("Fingerprint", "Firma digitale"), + ("Copy Fingerprint", "Còpia firma digitale"), + ("no fingerprints", "Peruna firma digitale"), + ("Select a peer", "Seletziona su dispositivu remotu"), + ("Select peers", "Seletziona sos dispositivos remotos"), + ("Plugins", "Cumplementos"), + ("Uninstall", "Disinstalla"), + ("Update", "Atualiza"), + ("Enable", "Abìlita"), + ("Disable", "Disabìlita"), + ("Options", "Optziones"), + ("resolution_original_tip", "Risolutzione originale"), + ("resolution_fit_local_tip", "Adata sa risolutzione locale"), + ("resolution_custom_tip", "Risolutzione personalizada"), + ("Collapse toolbar", "Mìnima s'istanga de sos trastes"), + ("Accept and Elevate", "Atzeta e cresche"), + ("accept_and_elevate_btn_tooltip", "Atzeta sa connessione e cresche sos permissos UAC."), + ("clipboard_wait_response_timeout_tip", "Tempus de isetu de rispota dae sa còpia iscadidu."), + ("Incoming connection", "Connessiones in intrada"), + ("Outgoing connection", "Connessiones in essida"), + ("Exit", "Essi dae RustDesk"), + ("Open", "Aberi RustDesk"), + ("logout_tip", "Ses seguru de bòlere essire?"), + ("Service", "Servìtziu"), + ("Start", "Allughe"), + ("Stop", "Firma"), + ("exceed_max_devices", "Ses arribbadu a su nùmeru màssimu de dispositivos chi podes manigiare."), + ("Sync with recent sessions", "Sincroniza cun sas sessiones reghentes"), + ("Sort tags", "Òrdina sas etichetas"), + ("Open connection in new tab", "Aberi sa connessione in un'ischeda noa"), + ("Move tab to new window", "Move s'ischeda a sa ventana imbeniente"), + ("Can not be empty", "Non podet èssere bòidu"), + ("Already exists", "Esistit giai"), + ("Change Password", "Modìfica sa crae"), + ("Refresh Password", "Annoa sa crae"), + ("ID", "ID"), + ("Grid View", "Vista grìllia"), + ("List View", "Vista elencu"), + ("Select", "Seletziona"), + ("Toggle Tags", "Allughe/istuda eticheta"), + ("pull_ab_failed_tip", "Non faghet a annoare sa rubrica"), + ("push_ab_failed_tip", "Non faghet a sincronizare sa rubrica cun su serbidore"), + ("synced_peer_readded_tip", "Sos dispositivos chi bi sunt in sas sessiones reghentes s'ant a torrare a sincronizare in sa rubrica."), + ("Change Color", "Modìfica colore"), + ("Primary Color", "Colore primàriu"), + ("HSV Color", "Colore HSV"), + ("Installation Successful!", "Installatzione cumprida"), + ("Installation failed!", "Installtazione fallida"), + ("Reverse mouse wheel", "Funtzione rodedda ratu furriada"), + ("{} sessions", "{} sessiones"), + ("scam_title", "Ti diant pòdere àere TRAMPADU!"), + ("scam_text1", "Si ses in su telèfonu cun calicunu chi NON connosches NON FIDADU chi t'at pedidu de impreare RustDesk e de allùghere su servìtziu, non sigas e tanca deretu."), + ("scam_text2", "Est dàbile chi siat unu trampadore chi chircat de furare su dinare tuo o àteras informatziones privadas tuas."), + ("Don't show again", "Non mustres prus"), + ("I Agree", "Atzeto"), + ("Decline", "No atzeto"), + ("Timeout in minutes", "Tempus de iscadèntzia in minutos"), + ("auto_disconnect_option_tip", "Serra in automàticu sas sessiones in intrada pro inatividade de s'utente"), + ("Connection failed due to inactivity", "Connessione non resèssida pro neghe de inatividade"), + ("Check for software update on startup", "A s'allughìngiu avèrgua sa presèntzia de atualizatziones pro su programma"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Atualiza RustDesk Server Pro a sa versione {} o prus noa!"), + ("pull_group_failed_tip", "Non faghet a annoare su grupu"), + ("Filter by intersection", "Filtra pro rugrada"), + ("Remove wallpaper during incoming sessions", "Boga s'isfundu durante sas essiones in intrada"), + ("Test", "Proa"), + ("display_is_plugged_out_msg", "S'ischermu est iscollegadu, colo a su primu ischermu."), + ("No displays", "Perunu ischermu"), + ("Open in new window", "Aberi in una ventana noa"), + ("Show displays as individual windows", "Mustra sos ischermos comente ventanas individuales"), + ("Use all my displays for the remote session", "In sa sessione remota imprea totu sos ischermos"), + ("selinux_tip", "In custu dispositivu est abilitadu SELinux, chi diat pòdere su funtzionamentu curretu de RustDesk comente ala controllada."), + ("Change view", "Modìfica vista"), + ("Big tiles", "Iconas mannas"), + ("Small tiles", "Iconas minores"), + ("List", "Elencu"), + ("Virtual display", "Ischermu virtuale"), + ("Plug out all", "Iscollega totu"), + ("True color (4:4:4)", "Colore reale (4:4:4)"), + ("Enable blocking user input", "Abìlita blocu insertada utente"), + ("id_input_tip", "Podes insertare un'ID, un'IP diretu o unu domìniu cun una ghenna (:).\nSi boles atzèdere a unu dispositivu in un'àteru serbidore, annanghe s'indiritzu de su serbidore (@?key=), a esèmpiu\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi boles atzèdere a unu dispositivu in unu serbidore pùblicu, inserta \"@public\", pro su serbidore pùblicu sa crae non serbit\n\nSi boles fortzare s'impreu de una connessione de inoltru a sa prima connessione, annanghe \"/r\" a sa fine de s'ID, a esèmpiu \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "Manera 1"), + ("privacy_mode_impl_virtual_display_tip", "Manera 2"), + ("Enter privacy mode", "Intra in modalidade de riservadesa"), + ("Exit privacy mode", "Essi dae sa modalidade de riservadesa"), + ("idd_not_support_under_win10_2004_tip", "Su driver vìdeu indiretu no est suportadu. Bisòngiat Windows 10, versione 2004 o prus noa."), + ("input_source_1_tip", "Fonte intrada (1)"), + ("input_source_2_tip", "Fonte intrada (2)"), + ("Swap control-command key", "Cuncàmbia tecla controllu-cumandu"), + ("swap-left-right-mouse", "Cuncàmbia pulsante mancu-destru ratu"), + ("2FA code", "Còdighe 2FA"), + ("More", "Àteru"), + ("enable-2fa-title", "Abìlita s'autenticatzione a duos fases"), + ("enable-2fa-desc", "Cunfigura s'autenticadore.\nPodes impreare un'aplicatzione de autenticatzione che a Authy, Microsoft o Google Authenticator in su telèfonu o elaboredore.\n\nPro abilitare s'autenticatzione a duas fases iscansi su còdighe QR cun s'aplicatzione e inserta su còdighe mustradu dae s'aplicatzione."), + ("wrong-2fa-code", "Non faghet a averguare su còdighe.\nVerìfica chi sas impostatziones de su còdighe e de s'ora locale siant curretas"), + ("enter-2fa-title", "Autenticatzione a duas fases"), + ("Email verification code must be 6 characters.", "Su còdighe de verìfica posta eletrònica depet cuntènnere 6 caràteres."), + ("2FA code must be 6 digits.", "Su còdighe 2FA depet èssere fatu de 6 tzifras."), + ("Multiple Windows sessions found", "Sessiones de Windows mùltiplas atzapadas"), + ("Please select the session you want to connect to", "Seletziona sa sessione cun chi ti boles cunnètere"), + ("powered_by_me", "Alimentadu dae RustDesk"), + ("outgoing_only_desk_tip", "Custa est un'editzione personalizada.\nTi podes connètere a àteros dispositivos, ma sos àteros dispositivos non si podent connètere a custu dispositivu."), + ("preset_password_warning", "Custa est un'editzione personalizada e benit frunida cun una crae de intrada pre-impostada.\nTotu sos chi connoschent custa crae diant pòdere otènnere su controllu totale de su dispositivu.\nSi non ti l'isetaias, disinstalla deretu su programma."), + ("Security Alert", "Avisu de seguresa"), + ("My address book", "Rubrica"), + ("Personal", "Personale"), + ("Owner", "Proprietàriu"), + ("Set shared password", "Imposta una crae cumpartzida"), + ("Exist in", "Esistit in"), + ("Read-only", "Leghidura ebbia"), + ("Read/Write", "Leghidura/iscritura"), + ("Full Control", "Controllu totale"), + ("share_warning_tip", "Sos campos inoghe in subra sunt cumpartzidos e sos àteros los pòdent bìdere."), + ("Everyone", "Totus"), + ("ab_web_console_tip", "Àteras informatziones subra de sa console web"), + ("allow-only-conn-window-open-tip", "Permite sa connessione petzi si sa ventana RustDesk est aberta"), + ("no_need_privacy_mode_no_physical_displays_tip", "Perunu ischermu fìsicu, peruna netzessidade de impreare sa modalidade de riservadesa."), + ("Follow remote cursor", "Sighi su cursore remotu"), + ("Follow remote window focus", "Sighi su focus de sa ventana remota"), + ("default_proxy_tip", "Protocollu e ghenna predefinidos sunt Socks5 e 1080"), + ("no_audio_input_device_tip", "Perunu dispositivu de intrada àudio atzapadu."), + ("Incoming", "In intrada"), + ("Outgoing", "In essida"), + ("Clear Wayland screen selection", "Annulla seletzione ischermada Wayland"), + ("clear_Wayland_screen_selection_tip", "A pustis de àere annulladu sa seletzione de ischermu, podes torrare a seletzionare s'ischermu de cumpartzire."), + ("confirm_clear_Wayland_screen_selection_tip", "Ses seguru de bòlere annullare sa seletzione de ischermu Wayland?"), + ("android_new_voice_call_tip", "As retzidu una rechuesta noa de mutida vocale. Si l'atzetas, sàudio at a colare a sa comunicatzione vocale."), + ("texture_render_tip", "Imprea sa tessidura de renderizatzione pro fàghere sas immàgines prus flùidas. Si atzapas problemas, proa a disabilitare custa optzione."), + ("Use texture rendering", "Imprea sa tessidura de renderizatzione"), + ("Floating window", "Ventana gallegiante"), + ("floating_window_tip", "Agiudat a mantènnere su servìtziu in s'isfundu de RustDesk"), + ("Keep screen on", "Mantene s'ischermu allutu"), + ("Never", "Mai"), + ("During controlled", "Durante su controllu"), + ("During service is on", "Cando su servìtziu est ativu"), + ("Capture screen using DirectX", "Catura s'ischermu impreende DirectX"), + ("Back", "In segus"), + ("Apps", "Aplicatziones"), + ("Volume up", "Volume +"), + ("Volume down", "Volume -"), + ("Power", "Alimentatzione"), + ("Telegram bot", "Bot de Telegram"), + ("enable-bot-tip", "Si abilitas custa funtzione, podes retzire su còdighe 2FA dae su bot tuo.\nPodes funtzionare fintzas comente notìfica de connessione."), + ("enable-bot-desc", "1. aberi una tzarrada cun @BotFather.\n2. Inbia su cumandu \"/newbot\", a pustis de àere fatu custu passàgiu as a retzire unu getone.\n3. Incumintza una tzarrada cun su bot tuo creadu como. Imbia unu messàgiu chi incumintzat cun un'istanga (\"/\") a tipu \"/salude\".\n"), + ("cancel-2fa-confirm-tip", "Ses seguru de bòlere annullare sa 2FA?"), + ("cancel-bot-confirm-tip", "Ses seguru de bòlere annullare Telegram?"), + ("About RustDesk", "Informatziones subra de RustDesk"), + ("Send clipboard keystrokes", "Imbia fileras teclas puntas de billete"), + ("network_error_tip", "Controlla sa connessione de rete, e a pustis seletziona 'Torra a proare'."), + ("Unlock with PIN", "Abìlita s'isblocu cun PIN"), + ("Requires at least {} characters", "Bisòngiant a su nessi {} caràteres"), + ("Wrong PIN", "PIN isballiadu"), + ("Set PIN", "Imposta su PIN"), + ("Enable trusted devices", "Abìlita dispositivos fidados"), + ("Manage trusted devices", "Manìgia sos dispositivos fidados"), + ("Platform", "Prataforma"), + ("Days remaining", "Dies chi abarrant"), + ("enable-trusted-devices-tip", "Brinca sa verìfica 2FA in sos dispositivos fidados"), + ("Parent directory", "Cartella printzipale"), + ("Resume", "Sighi"), + ("Invalid file name", "Nùmene archìviu non vàlidu"), + ("one-way-file-transfer-tip", "In s'ala controllada est abilitada sa tràmuda de archìvios a una diretzione ebbia."), + ("Authentication Required", "Dimanda de autenticatzione"), + ("Authenticate", "Autèntica"), + ("web_id_input_tip", "Podes insertare un'ID in su matessi serbidore, in su cliente web no est suportadu s'atzessu cun IP diretu.\nSi boles atzèdere a unu dispositivu in un'àteru serbidore, annanghe s'indiritzu de su serbidore (@?key=), a esèmpiu,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi boles intrare a unu dispositivu in unu serbidore pùblicu, inserta \"@public\", non b'at bisòngiu de sa crae pro su serbidore pùblicu."), + ("Download", "Iscàrriga"), + ("Upload folder", "Cartella de carrigamentu"), + ("Upload files", "Carrigamentu de archìvios upload"), + ("Clipboard is synchronized", "Sa punta de billete est sincronizada"), + ("Update client clipboard", "Annoa sa punta de billete de su cliente"), + ("Untagged", "Chene tag"), + ("new-version-of-{}-tip", "B'at una versione noa de {} a disponimentu"), + ("Accessible devices", "Dispositivos atzessìbiles"), + ].iter().cloned().collect(); +} From bfbf00f18cb711ad1ec7da92c0a561a79ec833c2 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 1 Mar 2025 19:43:01 +0800 Subject: [PATCH 121/506] fix: custom client, settings button (#10974) Signed-off-by: fufesou --- .../lib/desktop/pages/desktop_home_page.dart | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index ba724eed5ee..94e32575861 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -134,12 +134,17 @@ class _DesktopHomePageState extends State color: Theme.of(context).colorScheme.background, child: Stack( children: [ - SingleChildScrollView( - controller: _leftPaneScrollController, - child: Column( - key: _childKey, - children: children, - ), + Column( + children: [ + SingleChildScrollView( + controller: _leftPaneScrollController, + child: Column( + key: _childKey, + children: children, + ), + ), + Expanded(child: Container()) + ], ), if (isOutgoingOnly) Positioned( From 2b68c46fdcbc495cb0b88c84b883a9ee577bb949 Mon Sep 17 00:00:00 2001 From: Lynilia <89228568+Lynilia@users.noreply.github.com> Date: Sun, 2 Mar 2025 11:04:22 +0100 Subject: [PATCH 122/506] French localization rework (#10966) --- src/lang/fr.rs | 704 ++++++++++++++++++++++++------------------------- 1 file changed, 352 insertions(+), 352 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index d156e34bc82..23c482fc608 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -1,50 +1,50 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", "Statut"), + ("Status", "État"), ("Your Desktop", "Votre bureau"), - ("desk_tip", "Votre bureau est accessible via l'identifiant et le mot de passe ci-dessous."), + ("desk_tip", "Votre bureau est accessible via l’identifiant et le mot de passe ci-dessous."), ("Password", "Mot de passe"), ("Ready", "Prêt"), - ("Established", "Établi"), - ("connecting_status", "Connexion au réseau RustDesk..."), - ("Enable service", "Autoriser le service"), + ("Established", "Établie"), + ("connecting_status", "Connexion au réseau RustDesk…"), + ("Enable service", "Activer le service"), ("Start service", "Démarrer le service"), - ("Service is running", "Le service est en cours d'exécution"), - ("Service is not running", "Le service ne fonctionne pas"), - ("not_ready_status", "Pas prêt, veuillez vérifier la connexion réseau"), - ("Control Remote Desktop", "Contrôler le bureau à distance"), - ("Transfer file", "Transfert de fichiers"), + ("Service is running", "Le service est en cours d’exécution"), + ("Service is not running", "Le service est inactif"), + ("not_ready_status", "Pas prêt ; veuillez vérifier la connexion"), + ("Control Remote Desktop", "Contrôler un bureau à distance"), + ("Transfer file", "Transférer des fichiers"), ("Connect", "Se connecter"), ("Recent sessions", "Sessions récentes"), - ("Address book", "Carnet d'adresses"), + ("Address book", "Carnet d’adresses"), ("Confirmation", "Confirmation"), ("TCP tunneling", "Tunnel TCP"), - ("Remove", "Supprimer"), - ("Refresh random password", "Actualiser le mot de passe aléatoire"), + ("Remove", "Retirer"), + ("Refresh random password", "Générer un nouveau mot de passe aléatoire"), ("Set your own password", "Définir votre propre mot de passe"), ("Enable keyboard/mouse", "Activer le contrôle clavier/souris"), ("Enable clipboard", "Activer la synchronisation du presse-papier"), ("Enable file transfer", "Activer le transfert de fichiers"), ("Enable TCP tunneling", "Activer le tunnel TCP"), - ("IP Whitelisting", "Liste blanche IP"), - ("ID/Relay Server", "ID/Serveur Relais"), + ("IP Whitelisting", "Liste blanche d’adresses IP"), + ("ID/Relay Server", "Serveur ID/relais"), ("Import server config", "Importer la configuration du serveur"), ("Export Server Config", "Exporter la configuration du serveur"), ("Import server configuration successfully", "Configuration du serveur importée avec succès"), ("Export server configuration successfully", "Configuration du serveur exportée avec succès"), ("Invalid server configuration", "Configuration du serveur non valide"), - ("Clipboard is empty", "Presse-papier vide"), + ("Clipboard is empty", "Le presse-papier est vide"), ("Stop service", "Arrêter le service"), - ("Change ID", "Changer d'ID"), + ("Change ID", "Modifier l’ID"), ("Your new ID", "Votre nouvel ID"), ("length %min% to %max%", "longueur de %min% à %max%"), ("starts with a letter", "commence par une lettre"), ("allowed characters", "caractères autorisés"), - ("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, - (dash), _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur doit être comprise entre 6 et 16."), - ("Website", "Site Web"), - ("About", "À propos de"), - ("Slogan_tip", "Fait avec cœur dans ce monde chaotique !"), + ("id_change_tip", "Seuls les caractères a-z, A-Z, 0-9, - (trait d’union) et _ (tiret bas) sont autorisés. La première lettre doit être a-z ou A-Z. La longueur doit être comprise entre 6 et 16."), + ("Website", "Site web"), + ("About", "À propos"), + ("Slogan_tip", "Fait avec cœur dans ce monde chaotique !"), ("Privacy Statement", "Déclaration de confidentialité"), ("Mute", "Muet"), ("Build Date", "Date de compilation"), @@ -58,10 +58,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Server", "Serveur relais"), ("API Server", "Serveur API"), ("invalid_http", "Doit commencer par http:// ou https://"), - ("Invalid IP", "IP invalide"), - ("Invalid format", "Format invalide"), - ("server_not_support", "Pas encore supporté par le serveur"), - ("Not available", "Indisponible"), + ("Invalid IP", "IP non valide"), + ("Invalid format", "Format non valide"), + ("server_not_support", "Non encore pris en charge par le serveur"), + ("Not available", "Non disponible"), ("Too frequent", "Modifié trop fréquemment, veuillez réessayer plus tard"), ("Cancel", "Annuler"), ("Skip", "Ignorer"), @@ -72,16 +72,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter your password", "Veuillez saisir votre mot de passe"), ("Remember password", "Mémoriser le mot de passe"), ("Wrong Password", "Mauvais mot de passe"), - ("Do you want to enter again?", "Voulez-vous participer à nouveau ?"), + ("Do you want to enter again?", "Voulez-vous ressaisir le mot de passe ?"), ("Connection Error", "Erreur de connexion"), ("Error", "Erreur"), - ("Reset by the peer", "La connexion a été fermée par l'appareil distant"), - ("Connecting...", "Connexion..."), - ("Connection in progress. Please wait.", "Connexion en cours. Veuillez patienter."), - ("Please try 1 minute later", "Réessayez dans une minute"), + ("Reset by the peer", "Terminée par l’appareil distant"), + ("Connecting...", "Connexion…"), + ("Connection in progress. Please wait.", "Connexion en cours ; veuillez patienter."), + ("Please try 1 minute later", "Veuillez réessayer dans une minute"), ("Login Error", "Erreur de connexion"), ("Successful", "Succès"), - ("Connected, waiting for image...", "Connecté, en attente de transmission d'image..."), + ("Connected, waiting for image...", "Connecté ; en attente de l’image…"), ("Name", "Nom"), ("Type", "Type"), ("Modified", "Modifié le"), @@ -101,70 +101,70 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select All", "Tout sélectionner"), ("Unselect All", "Tout déselectionner"), ("Empty Directory", "Répertoire vide"), - ("Not an empty directory", "Pas un répertoire vide"), - ("Are you sure you want to delete this file?", "Voulez-vous vraiment supprimer ce fichier ?"), + ("Not an empty directory", "Répertoire non vide"), + ("Are you sure you want to delete this file?", "Voulez-vous vraiment supprimer ce fichier ?"), ("Are you sure you want to delete this empty directory?", "Voulez-vous vraiment supprimer ce répertoire vide ?"), ("Are you sure you want to delete the file of this directory?", "Voulez-vous vraiment supprimer le fichier de ce répertoire ?"), - ("Do this for all conflicts", "Appliquer à d'autres conflits"), - ("This is irreversible!", "C'est irréversible !"), + ("Do this for all conflicts", "Appliquer à tous les conflits"), + ("This is irreversible!", "Ceci est irréversible !"), ("Deleting", "Suppression"), - ("files", "fichier"), - ("Waiting", "En attente..."), + ("files", "fichiers"), + ("Waiting", "En attente"), ("Finished", "Terminé"), ("Speed", "Vitesse"), - ("Custom Image Quality", "Définir la qualité d'image"), - ("Privacy mode", "Mode privé"), - ("Block user input", "Bloquer la saisie de l'utilisateur"), - ("Unblock user input", "Débloquer l'entrée de l'utilisateur"), + ("Custom Image Quality", "Qualité d’image personnalisée"), + ("Privacy mode", "Mode de confidentialité"), + ("Block user input", "Bloquer la saisie de l’utilisateur"), + ("Unblock user input", "Débloquer la saisie de l’utilisateur"), ("Adjust Window", "Ajuster la fenêtre"), ("Original", "Ratio d'origine"), ("Shrink", "Rétrécir"), ("Stretch", "Étirer"), ("Scrollbar", "Barre de défilement"), ("ScrollAuto", "Défilement automatique"), - ("Good image quality", "Bonne qualité d'image"), - ("Balanced", "Qualité d'image normale"), + ("Good image quality", "Bonne qualité d’image"), + ("Balanced", "Équilibré"), ("Optimize reaction time", "Optimiser le temps de réaction"), ("Custom", "Personnalisé"), ("Show remote cursor", "Afficher le curseur distant"), ("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"), + ("Lock after session end", "Verrouiller l’appareil distant après la déconnexion"), ("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"), - ("Failed to connect to rendezvous server", "Échec de la connexion au serveur rendezvous"), + ("Insert Lock", "Verrouiller l’appareil distant"), + ("Refresh", "Rafraîchir l’écran"), + ("ID does not exist", "L’ID n’existe pas"), + ("Failed to connect to rendezvous server", "Échec de la connexion au serveur de rendez-vous"), ("Please try later", "Veuillez essayer plus tard"), - ("Remote desktop is offline", "Le bureau à distance est hors ligne"), - ("Key mismatch", "Discordance de clés"), + ("Remote desktop is offline", "Le bureau distant est hors ligne"), + ("Key mismatch", "Discordance des clés"), ("Timeout", "Connexion expirée"), ("Failed to connect to relay server", "Échec de la connexion au serveur relais"), - ("Failed to connect via rendezvous server", "Échec de l'établissement d'une connexion via le serveur rendezvous"), - ("Failed to connect via relay server", "Impossible d'établir une connexion via le serveur relais"), - ("Failed to make direct connection to remote desktop", "Impossible d'établir une connexion directe"), + ("Failed to connect via rendezvous server", "Échec de la connexion via le serveur de rendez-vous"), + ("Failed to connect via relay server", "Échec de la connexion via le serveur relais"), + ("Failed to make direct connection to remote desktop", "Échec de la connexion directe au bureau distant"), ("Set Password", "Définir le mot de passe"), - ("OS Password", "Mot de passe du système d'exploitation"), - ("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"), + ("OS Password", "Mot de passe du système d’exploitation"), + ("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", "Mettre à niveau"), + ("Click to download", "Télécharger"), + ("Click to update", "Mettre à jour"), ("Configure", "Configurer"), - ("config_acc", "Afin de pouvoir contrôler votre bureau à distance, veuillez donner l'autorisation \"accessibilité\" à RustDesk."), - ("config_screen", "Afin de pouvoir accéder à votre bureau à distance, veuillez donner à RustDesk l'autorisation \"enregistrement d'écran\"."), - ("Installing ...", "Installation..."), + ("config_acc", "L’autorisation « Accessibilité » est requise pour contrôler votre bureau à distance."), + ("config_screen", "L’autorisation « Enregistrement d’écran » est requise pour accéder à votre bureau à distance."), + ("Installing ...", "Installation…"), ("Install", "Installer"), ("Installation", "Installation"), - ("Installation Path", "Chemin d'installation"), + ("Installation Path", "Chemin d’installation"), ("Create start menu shortcuts", "Créer des raccourcis dans le menu démarrer"), ("Create desktop icon", "Créer une icône sur le bureau"), - ("agreement_tip", "Démarrer l'installation signifie accepter le contrat de licence."), + ("agreement_tip", "En lançant l’installation, vous acceptez le contrat de licence."), ("Accept and Install", "Accepter et installer"), - ("End-user license agreement", "Contrat d'utilisateur"), - ("Generating ...", "Génération..."), - ("Your installation is lower version.", "La version que vous avez installée est inférieure à la version en cours d'exécution."), - ("not_close_tcp_tip", "Veuillez ne pas fermer cette fenêtre lors de l'utilisation du tunnel"), - ("Listening ...", "En attente de connexion tunnel..."), + ("End-user license agreement", "Conditions générales d’utilisation"), + ("Generating ...", "Génération…"), + ("Your installation is lower version.", "La version installée est antérieure à la version en cours d’exécution."), + ("not_close_tcp_tip", "Veuillez ne pas fermer cette fenêtre lors de l’utilisation du tunnel"), + ("Listening ...", "En attente de connexion…"), ("Remote Host", "Hôte distant"), ("Remote Port", "Port distant"), ("Action", "Action"), @@ -172,90 +172,90 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Port local"), ("Local Address", "Adresse locale"), ("Change Local Port", "Changer le port local"), - ("setup_server_tip", "Si vous avez besoin d'une vitesse de connexion plus rapide, vous pouvez choisir de créer votre propre serveur"), - ("Too short, at least 6 characters.", "Trop court, au moins 6 caractères."), - ("The confirmation is not identical.", "Les deux entrées ne correspondent pas"), + ("setup_server_tip", "N’hésitez pas à mettre en place votre propre serveur afin d’améliorer la connexion"), + ("Too short, at least 6 characters.", "Trop court, 6 caractères minimum."), + ("The confirmation is not identical.", "Les deux entrées ne correspondent pas."), ("Permissions", "Autorisations"), ("Accept", "Accepter"), ("Dismiss", "Rejeter"), ("Disconnect", "Déconnecter"), - ("Enable file copy and paste", "Autoriser le copier-coller de fichiers"), + ("Enable file copy and paste", "Activer le copier-coller de fichiers"), ("Connected", "Connecté"), ("Direct and encrypted connection", "Connexion directe chiffrée"), - ("Relayed and encrypted connection", "Connexion relais chiffrée"), + ("Relayed and encrypted connection", "Connexion via relais chiffrée"), ("Direct and unencrypted connection", "Connexion directe non chiffrée"), - ("Relayed and unencrypted connection", "Connexion relais non chiffrée"), - ("Enter Remote ID", "Entrer l'ID de l'appareil distant"), - ("Enter your password", "Entrer votre mot de passe"), - ("Logging in...", "En cours de connexion ..."), + ("Relayed and unencrypted connection", "Connexion via relais non chiffrée"), + ("Enter Remote ID", "Saisissez l’ID de l’appareil distant"), + ("Enter your password", "Saisissez votre mot de passe"), + ("Logging in...", "En cours de connexion…"), ("Enable RDP session sharing", "Activer le partage de session RDP"), - ("Auto Login", "Connexion automatique (le verrouillage ne sera effectif qu'après la désactivation du premier paramètre)"), - ("Enable direct IP access", "Autoriser l'accès direct par IP"), + ("Auto Login", "Connexion automatique (Requiert l’activation de l’option « Verrouiller l’appareil distant après la déconnexion »)"), + ("Enable direct IP access", "Activer l’accès direct par adresse IP"), ("Rename", "Renommer"), ("Space", "Espace"), ("Create desktop shortcut", "Créer un raccourci sur le bureau"), - ("Change Path", "Changer de chemin"), + ("Change Path", "Modifier le chemin"), ("Create Folder", "Créer un dossier"), ("Please enter the folder name", "Veuillez saisir le nom du dossier"), ("Fix it", "Réparer"), ("Warning", "Avertissement"), - ("Login screen using Wayland is not supported", "L'écran de connexion utilisant Wayland n'est pas pris en charge"), + ("Login screen using Wayland is not supported", "L’écran de connexion n’est pas pris en charge sous Wayland"), ("Reboot required", "Redémarrage requis"), - ("Unsupported display server", "Le serveur d'affichage actuel n'est pas pris en charge"), - ("x11 expected", "x11 requis"), + ("Unsupported display server", "Le serveur d’affichage n’est pas pris en charge"), + ("x11 expected", "x11 attendu"), ("Port", "Port"), ("Settings", "Paramètres"), - ("Username", " Nom d'utilisateur"), - ("Invalid port", "Port invalide"), - ("Closed manually by the peer", "Fermé manuellement par l'appareil distant"), - ("Enable remote configuration modification", "Autoriser la modification de la configuration à distance"), + ("Username", " Nom d’utilisateur"), + ("Invalid port", "Port non valide"), + ("Closed manually by the peer", "Terminée manuellement par l’appareil distant"), + ("Enable remote configuration modification", "Activer la modification de la configuration à distance"), ("Run without install", "Exécuter sans installer"), - ("Connect via relay", "Connexion via relais"), - ("Always connect via relay", "Forcer la connexion relais"), - ("whitelist_tip", "Seule une IP de la liste blanche peut accéder à mon appareil"), + ("Connect via relay", "Connecter via relais"), + ("Always connect via relay", "Forcer la connexion via relais"), + ("whitelist_tip", "Seules les adresses IP incluses dans la liste blanche pourront accéder à mon appareil"), ("Login", "Connexion"), ("Verify", "Vérifier"), ("Remember me", "Se souvenir de moi"), ("Trust this device", "Faire confiance à cet appareil"), ("Verification code", "Code de vérification"), - ("verification_tip", "Un nouvel appareil a été détecté et un code de vérification a été envoyé à l'adresse e-mail enregistrée, entrez le code de vérification pour continuer la connexion."), + ("verification_tip", "Un code de vérification a été envoyé à l’adresse électronique enregistrée ; saisissez le code de vérification afin de poursuivre la connexion."), ("Logout", "Déconnexion"), ("Tags", "Étiquettes"), ("Search ID", "Rechercher un ID"), ("whitelist_sep", "Vous pouvez utiliser une virgule, un point-virgule, un espace ou une nouvelle ligne comme séparateur"), ("Add ID", "Ajouter un ID"), - ("Add Tag", "Ajout étiquette(s)"), + ("Add Tag", "Ajouter une étiquette"), ("Unselect all tags", "Désélectionner toutes les étiquettes"), ("Network error", "Erreur réseau"), - ("Username missed", "Nom d'utilisateur manquant"), + ("Username missed", "Nom d’utilisateur manquant"), ("Password missed", "Mot de passe manquant"), ("Wrong credentials", "Identifiant ou mot de passe erroné"), ("The verification code is incorrect or has expired", "Le code de vérification est incorrect ou a expiré"), - ("Edit Tag", "Gestion étiquettes"), - ("Forget Password", "Oublier le Mot de passe"), + ("Edit Tag", "Modifier l’étiquette"), + ("Forget Password", "Oublier le mot de passe"), ("Favorites", "Favoris"), - ("Add to Favorites", "Ajouter aux Favoris"), + ("Add to Favorites", "Ajouter aux favoris"), ("Remove from Favorites", "Retirer des favoris"), ("Empty", "Vide"), - ("Invalid folder name", "Nom de dossier invalide"), + ("Invalid folder name", "Nom de dossier non valide"), ("Socks5 Proxy", "Socks5 Agents"), - ("Socks5/Http(s) Proxy", "Socks5/Http(s) Agents"), - ("Discovered", "Découvert"), - ("install_daemon_tip", "Pour une exécution au démarrage du système, vous devez installer le service système."), - ("Remote ID", "ID de l'appareil distant"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), + ("Discovered", "Découverts"), + ("install_daemon_tip", "Le service système doit être installé avant de pouvoir activer l’exécution au démarrage du système."), + ("Remote ID", "ID de l’appareil distant"), ("Paste", "Coller"), - ("Paste here?", "Coller ici ?"), - ("Are you sure to close the connection?", "Êtes-vous sûr de fermer la connexion ?"), + ("Paste here?", "Coller ici ?"), + ("Are you sure to close the connection?", "Voulez-vous vraiment terminer la connexion ?"), ("Download new version", "Télécharger la nouvelle version"), ("Touch mode", "Mode tactile"), ("Mouse mode", "Mode souris"), - ("One-Finger Tap", "Tapez d'un doigt"), - ("Left Mouse", "Bouton gauche de la souris"), - ("One-Long Tap", "Un touché long"), - ("Two-Finger Tap", "Tapez à deux doigts"), - ("Right Mouse", "Bouton droit de la souris"), + ("One-Finger Tap", "Appui simple"), + ("Left Mouse", "Clic gauche"), + ("One-Long Tap", "Appui prolongé"), + ("Two-Finger Tap", "Appui à deux doigts"), + ("Right Mouse", "Clic droit"), ("One-Finger Move", "Mouvement à un doigt"), - ("Double Tap & Move", "Appuyez deux fois et déplacez"), + ("Double Tap & Move", "Mouvement après double appui"), ("Mouse Drag", "Glissement de la souris"), ("Three-Finger vertically", "Trois doigts verticalement"), ("Mouse Wheel", "Roulette de la souris"), @@ -264,80 +264,80 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Pinch to Zoom", "Pincer pour zoomer"), ("Canvas Zoom", "Zoom sur la vue"), ("Reset canvas", "Réinitialiser la vue"), - ("No permission of file transfer", "Aucune autorisation de transfert de fichiers"), - ("Note", "Noter"), + ("No permission of file transfer", "Absence de l’autorisation de transfert de fichiers"), + ("Note", "Note"), ("Connection", "Connexion"), - ("Share Screen", "Partager l'écran"), - ("Chat", "Discuter"), + ("Share Screen", "Partage d’écran"), + ("Chat", "Discussion"), ("Total", "Total"), ("items", "éléments"), ("Selected", "Sélectionné(s)"), - ("Screen Capture", "Capture d'écran"), - ("Input Control", "Contrôle de saisie"), - ("Audio Capture", "Capture audio"), - ("File Connection", "Connexion de fichier"), - ("Screen Connection", "Connexion de l'écran"), - ("Do you accept?", "Acceptez-vous ?"), + ("Screen Capture", "Capture de l’écran"), + ("Input Control", "Contrôle de la saisie"), + ("Audio Capture", "Capture de l’audio"), + ("File Connection", "Connexion aux fichiers"), + ("Screen Connection", "Connexion à l’écran"), + ("Do you accept?", "Acceptez-vous ?"), ("Open System Setting", "Ouvrir les paramètres système"), - ("How to get Android input permission?", "Comment obtenir l'autorisation d'entrée Android ?"), - ("android_input_permission_tip1", "Pour qu'un appareil distant puisse contrôler votre appareil Android via la souris ou le toucher, vous devez autoriser RustDesk à utiliser le service \"Accessibilité\"."), - ("android_input_permission_tip2", "Veuillez accéder à la page suivante des paramètres système, recherchez et entrez [Services installés], activez le service [RustDesk Input]."), + ("How to get Android input permission?", "Comment obtenir l’autorisation de contrôle de la saisie sur Android ?"), + ("android_input_permission_tip1", "Pour qu’un appareil distant puisse contrôler votre appareil Android via la souris ou le toucher d’écran, vous devez autoriser RustDesk à utiliser le service « Accessibilité »."), + ("android_input_permission_tip2", "Veuillez accéder à la page suivante des paramètres système, puis recherchez et accédez à la section [Services installés] ; activez ensuite le service [RustDesk Input]."), ("android_new_connection_tip", "Une nouvelle demande de contrôle a été reçue, elle souhaite contrôler votre appareil actuel."), - ("android_service_will_start_tip", "L'activation de la capture d'écran démarrera automatiquement le service, permettant à d'autres appareils de demander une connexion à partir de cet appareil."), - ("android_stop_service_tip", "La fermeture du service fermera automatiquement toutes les connexions établies."), - ("android_version_audio_tip", "La version actuelle d'Android ne prend pas en charge la capture audio, veuillez passer à Android 10 ou supérieur."), - ("android_start_service_tip", "Appuyez sur [Démarrer le service] ou activez l'autorisation [Capture d'écran] pour démarrer le service de partage d'écran."), - ("android_permission_may_not_change_tip", "Les autorisations pour les connexions établies peuvent ne pas être prisent en compte instantanément ou avant la reconnection."), + ("android_service_will_start_tip", "L’activation de la capture de l’écran démarrera automatiquement le service, ce qui permettra aux appareils distants d’initier une connexion vers cet appareil."), + ("android_stop_service_tip", "L’arrêt du service terminera automatiquement toutes les connexions établies."), + ("android_version_audio_tip", "La version actuelle d’Android ne prend pas en charge la capture de l’audio, veuillez passer à Android 10 ou supérieur."), + ("android_start_service_tip", "Appuyez sur [Démarrer le service] ou activez l’autorisation [Capture de l’écran] pour démarrer le service de partage d’écran."), + ("android_permission_may_not_change_tip", "Les modifications des autorisations peuvent requérir une reconnexion avant d’être prises en compte par les connexions déjà établies."), ("Account", "Compte"), ("Overwrite", "Écraser"), - ("This file exists, skip or overwrite this file?", "Ce fichier existe, ignorer ou écraser ce fichier ?"), + ("This file exists, skip or overwrite this file?", "Ce fichier existe déjà, ignorer ou écraser ce fichier ?"), ("Quit", "Quitter"), - ("Help", "Aider"), - ("Failed", "échouer"), + ("Help", "Aide"), + ("Failed", "Échec"), ("Succeeded", "Succès"), - ("Someone turns on privacy mode, exit", "Quelqu'un active le mode de confidentialité, quittez"), + ("Someone turns on privacy mode, exit", "Quelqu’un active le mode de confidentialité, désactiver"), ("Unsupported", "Non pris en charge"), - ("Peer denied", "Appareil distant refusé"), + ("Peer denied", "Refusé par l’appareil distant"), ("Please install plugins", "Veuillez installer les plugins"), - ("Peer exit", "Appareil distant déconnecté"), + ("Peer exit", "Désactivé par l’appareil distant"), ("Failed to turn off", "Échec de la désactivation"), ("Turned off", "Désactivé"), ("Language", "Langue"), - ("Keep RustDesk background service", "Gardez le service RustDesk en arrière plan"), - ("Ignore Battery Optimizations", "Ignorer les optimisations batterie"), - ("android_open_battery_optimizations_tip", "Conseil android d'optimisation de batterie"), + ("Keep RustDesk background service", "Garder le service RustDesk en arrière plan"), + ("Ignore Battery Optimizations", "Ignorer les optimisations de la batterie"), + ("android_open_battery_optimizations_tip", "Pour désactiver cette fonctionnalité, veuillez accéder à la page suivante des paramètres de l’application RustDesk, puis recherchez et accédez à la section [Batterie] ; décochez ensuite l’option [Sans restriction]."), ("Start on boot", "Lancer au démarrage"), - ("Start the screen sharing service on boot, requires special permissions", "Lancer le service de partage d'écran au démarrage, nécessite des autorisations spéciales"), + ("Start the screen sharing service on boot, requires special permissions", "Lancer le service de partage d’écran au démarrage, nécessite des autorisations spéciales"), ("Connection not allowed", "Connexion non autorisée"), ("Legacy mode", "Mode hérité"), ("Map mode", "Mode correspondance"), ("Translate mode", "Mode traduction"), ("Use permanent password", "Utiliser un mot de passe permanent"), - ("Use both passwords", "Utiliser les mots de passe unique et permanent"), + ("Use both passwords", "Utiliser les deux mots de passe"), ("Set permanent password", "Définir le mot de passe permanent"), ("Enable remote restart", "Activer le redémarrage à distance"), - ("Restart remote device", "Redémarrer l'appareil à distance"), - ("Are you sure you want to restart", "Êtes-vous sûr de vouloir redémarrer l'appareil ?"), - ("Restarting remote device", "Redémarrage de l'appareil distant"), - ("remote_restarting_tip", "L'appareil distant redémarre, veuillez fermer cette boîte de message et vous reconnecter avec un mot de passe permanent après un certain temps"), + ("Restart remote device", "Redémarrer l’appareil distant"), + ("Are you sure you want to restart", "Voulez-vous vraiment redémarrer l’appareil ?"), + ("Restarting remote device", "Redémarrage de l’appareil distant"), + ("remote_restarting_tip", "L'appareil distant redémarre ; veuillez fermer cette boîte de dialogue et vous reconnecter en utilisant le mot de passe permanent dans quelques instants"), ("Copied", "Copié"), ("Exit Fullscreen", "Quitter le mode plein écran"), ("Fullscreen", "Plein écran"), ("Mobile Actions", "Actions mobiles"), - ("Select Monitor", "Sélection du Moniteur"), + ("Select Monitor", "Sélection du moniteur"), ("Control Actions", "Actions de contrôle"), - ("Display Settings", "Paramètres d'affichage"), + ("Display Settings", "Paramètres d’affichage"), ("Ratio", "Rapport"), - ("Image Quality", "Qualité d'image"), + ("Image Quality", "Qualité d’image"), ("Scroll Style", "Style de défilement"), - ("Show Toolbar", "Afficher la barre d'outils"), - ("Hide Toolbar", "Masquer la barre d'outils"), + ("Show Toolbar", "Afficher la barre d’outils"), + ("Hide Toolbar", "Cacher la barre d’outils"), ("Direct Connection", "Connexion directe"), - ("Relay Connection", "Connexion relais"), + ("Relay Connection", "Connexion via relais"), ("Secure Connection", "Connexion sécurisée"), ("Insecure Connection", "Connexion non sécurisée"), - ("Scale original", "Échelle 100%"), - ("Scale adaptive", "Mise à l'échelle Auto"), + ("Scale original", "Échelle 100 %"), + ("Scale adaptive", "Mise à l’échelle auto"), ("General", "Général"), ("Security", "Sécurité"), ("Theme", "Thème"), @@ -347,315 +347,315 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Light", "Clair"), ("Follow System", "Suivi système"), ("Enable hardware codec", "Activer le transcodage matériel"), - ("Unlock Security Settings", "Déverrouiller les configurations de sécurité"), - ("Enable audio", "Activer l'audio"), - ("Unlock Network Settings", "Déverrouiller les configurations réseau"), + ("Unlock Security Settings", "Déverrouiller les paramètres de sécurité"), + ("Enable audio", "Activer l’audio"), + ("Unlock Network Settings", "Déverrouiller les paramètres réseau"), ("Server", "Serveur"), - ("Direct IP Access", "Accès IP direct"), + ("Direct IP Access", "Accès direct par adresse IP"), ("Proxy", "Proxy"), ("Apply", "Appliquer"), - ("Disconnect all devices?", "Déconnecter tous les appareils ?"), + ("Disconnect all devices?", "Déconnecter tous les appareils ?"), ("Clear", "Effacer"), ("Audio Input Device", "Périphérique source audio"), - ("Use IP Whitelisting", "Utiliser une liste blanche d'IP"), + ("Use IP Whitelisting", "Utiliser une liste blanche d’adresses IP"), ("Network", "Réseau"), - ("Pin Toolbar", "Épingler la barre d'outil"), - ("Unpin Toolbar", "Détacher la barre d'outil"), + ("Pin Toolbar", "Épingler la barre d’outils"), + ("Unpin Toolbar", "Détacher la barre d’outils"), ("Recording", "Enregistrement"), ("Directory", "Répertoire"), - ("Automatically record incoming sessions", "Enregistrement automatique des sessions entrantes"), - ("Automatically record outgoing sessions", ""), + ("Automatically record incoming sessions", "Enregistrer automatiquement les sessions entrantes"), + ("Automatically record outgoing sessions", "Enregistrer automatiquement les sessions sortantes"), ("Change", "Modifier"), - ("Start session recording", "Commencer l'enregistrement"), - ("Stop session recording", "Stopper l'enregistrement"), - ("Enable recording session", "Activer l'enregistrement de session"), + ("Start session recording", "Commencer l’enregistrement"), + ("Stop session recording", "Stopper l’enregistrement"), + ("Enable recording session", "Activer l’enregistrement de session"), ("Enable LAN discovery", "Activer la découverte sur réseau local"), - ("Deny LAN discovery", "Interdir la découverte sur réseau local"), - ("Write a message", "Ecrire un message"), + ("Deny LAN discovery", "Interdire la découverte sur réseau local"), + ("Write a message", "Écrire un message"), ("Prompt", "Annonce"), - ("Please wait for confirmation of UAC...", "Veuillez attendre la confirmation de l'UAC..."), - ("elevated_foreground_window_tip", "La fenêtre actuelle de l'appareil distant nécessite des privilèges plus élevés pour fonctionner, elle ne peut donc pas être atteinte par la souris et le clavier. Vous pouvez demander à l'utilisateur distant de réduire la fenêtre actuelle ou de cliquer sur le bouton d'élévation dans la fenêtre de gestion des connexions. Pour éviter ce problème, il est recommandé d'installer le logiciel sur l'appareil distant."), + ("Please wait for confirmation of UAC...", "Veuillez attendre la confirmation de l’UAC…"), + ("elevated_foreground_window_tip", "La fenêtre active du bureau distant nécessite des privilèges plus élevés pour fonctionner, la souris et le clavier ne peuvent donc pas l’atteindre actuellement. Vous pouvez demander à l’utilisateur distant de réduire la fenêtre active ou de cliquer sur le bouton d’élévation dans la fenêtre de gestion de la connexion. Il est conseillé d’installer le logiciel sur l’appareil distant afin d’éviter ce problème."), ("Disconnected", "Déconnecté"), ("Other", "Divers"), ("Confirm before closing multiple tabs", "Confirmer avant de fermer plusieurs onglets"), - ("Keyboard Settings", "Configuration clavier"), + ("Keyboard Settings", "Paramètres du clavier"), ("Full Access", "Accès total"), - ("Screen Share", "Partage d'écran"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nécessite Ubuntu 21.04 ou une version supérieure."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nécessite une version supérieure de la distribution Linux. Veuillez essayer le bureau X11 ou changer votre système d'exploitation."), + ("Screen Share", "Partage d’écran"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nécessite Ubuntu 21.04 ou une version ultérieure."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nécessite une version ultérieure de votre distribution Linux. Veuillez essayer le bureau X11 ou changer de système d’exploitation."), ("JumpLink", "Afficher"), - ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l'écran à partager (côté appareil distant)."), + ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l’écran à partager (côté appareil distant)."), ("Show RustDesk", "Afficher RustDesk"), ("This PC", "Ce PC"), ("or", "ou"), ("Continue with", "Continuer avec"), - ("Elevate", "Autoriser l'accès"), + ("Elevate", "Élever les privilèges"), ("Zoom cursor", "Augmenter la taille du curseur"), ("Accept sessions via password", "Accepter les sessions via mot de passe"), - ("Accept sessions via click", "Accepter les sessions via clique de confirmation"), - ("Accept sessions via both", "Accepter les sessions via mot de passe ou clique de confirmation"), - ("Please wait for the remote side to accept your session request...", "Veuillez attendre que votre demande de session distante soit accepter ..."), - ("One-time Password", "Mot de passe unique"), - ("Use one-time password", "Utiliser un mot de passe unique"), - ("One-time password length", "Longueur du mot de passe unique"), - ("Request access to your device", "Demande d'accès à votre appareil"), - ("Hide connection management window", "Masquer la fenêtre de gestion des connexions"), - ("hide_cm_tip", "Autoriser le masquage uniquement si vous acceptez des sessions via un mot de passe et utilisez un mot de passe permanent"), - ("wayland_experiment_tip", "Le support Wayland est en phase expérimentale, veuillez utiliser X11 si vous avez besoin d'un accès sans surveillance."), - ("Right click to select tabs", "Clique droit pour selectionner les onglets"), + ("Accept sessions via click", "Accepter les sessions via clic de confirmation"), + ("Accept sessions via both", "Accepter les sessions via mot de passe ou clic de confirmation"), + ("Please wait for the remote side to accept your session request...", "Veuillez attendre que votre demande de session distante soit acceptée…"), + ("One-time Password", "Mot de passe à usage unique"), + ("Use one-time password", "Utiliser un mot de passe à usage unique"), + ("One-time password length", "Longueur du mot de passe à usage unique"), + ("Request access to your device", "Demande l’accès à votre appareil"), + ("Hide connection management window", "Cacher la fenêtre de gestion de la connexion"), + ("hide_cm_tip", "Requiert d’accepter les sessions via mot de passe avec un mot de passe permanent"), + ("wayland_experiment_tip", "La prise en charge de Wayland est en phase expérimentale, veuillez utiliser X11 si vous avez besoin d’un accès non assisté."), + ("Right click to select tabs", "Clic droit pour sélectionner les onglets"), ("Skipped", "Ignoré"), - ("Add to address book", "Ajouter au carnet d'adresses"), + ("Add to address book", "Ajouter au carnet d’adresses"), ("Group", "Groupe"), ("Search", "Rechercher"), - ("Closed manually by web console", "Fermé manuellement par la console Web"), + ("Closed manually by web console", "Terminée manuellement par la console web"), ("Local keyboard type", "Disposition du clavier local"), - ("Select local keyboard type", "Selectionner la disposition du clavier local"), - ("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), - ("Always use software rendering", "Utiliser toujours le rendu logiciel"), - ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à RustDesk l'autorisation \"Surveillance de l’entrée\"."), - ("config_microphone", "Pour discuter à distance, vous devez accorder à RustDesk les autorisations « Enregistrer l'audio »."), - ("request_elevation_tip", "Vous pouvez également demander une augmentation des privilèges s'il y a quelqu'un du côté distant."), - ("Wait", "En cours"), - ("Elevation Error", "Erreur d'augmentation des privilèges"), - ("Ask the remote user for authentication", "Demander à l'utilisateur distant de s'authentifier"), - ("Choose this if the remote account is administrator", "Choisissez ceci si le compte distant est le compte d'administrateur"), - ("Transmit the username and password of administrator", "Transmettre le nom d'utilisateur et le mot de passe de l'administrateur"), - ("still_click_uac_tip", "Nécessite toujours que l'utilisateur distant confirme par la fenêtre UAC de RustDesk en cours d'éxécution."), - ("Request Elevation", "Demande d'augmentation des privilèges"), - ("wait_accept_uac_tip", "Veuillez attendre que l'utilisateur distant accepte la boîte de dialogue UAC."), - ("Elevate successfully", "Augmentation des privilèges avec succès"), + ("Select local keyboard type", "Sélectionner la disposition du clavier local"), + ("software_render_tip", "Si vous utilisez une carte graphique Nvidia sous Linux et que la fenêtre distante se ferme immédiatement après la connexion, l’installation du pilote open-source Nouveau et l’utilisation du rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), + ("Always use software rendering", "Toujours utiliser le rendu logiciel"), + ("config_input", "Vous devez accorder à RustDesk l’autorisation « Surveillance de l’entrée » pour contrôler le bureau distant avec le clavier."), + ("config_microphone", "Vous devez accorder à RustDesk l’autorisation « Enregistrer l’audio » pour discuter à distance."), + ("request_elevation_tip", "Vous pouvez également demander une élévation des privilèges si un utilisateur est présent côté distant."), + ("Wait", "Attendre"), + ("Elevation Error", "Erreur d’élévation des privilèges"), + ("Ask the remote user for authentication", "Demander à l’utilisateur distant de s’authentifier"), + ("Choose this if the remote account is administrator", "Sélectionnez cette option si le compte distant est administrateur"), + ("Transmit the username and password of administrator", "Transmettre le nom d’utilisateur et le mot de passe d’un compte administrateur"), + ("still_click_uac_tip", "L’utilisateur distant devra malgré tout confirmer l’UAC de l’instance RustDesk en cours d’éxécution."), + ("Request Elevation", "Demander l’élévation des privilèges"), + ("wait_accept_uac_tip", "Veuillez attendre l’acceptation de l’UAC par l’utilisateur distant."), + ("Elevate successfully", "Élévation des privilèges réussie"), ("uppercase", "majuscule"), ("lowercase", "minuscule"), ("digit", "chiffre"), ("special character", "caractère spécial"), - ("length>=8", "longueur>=8"), + ("length>=8", "longueur ≥ 8"), ("Weak", "Faible"), ("Medium", "Moyen"), ("Strong", "Fort"), ("Switch Sides", "Inverser la prise de contrôle"), - ("Please confirm if you want to share your desktop?", "Veuillez confirmer le partager de votre bureau ?"), + ("Please confirm if you want to share your desktop?", "Voulez-vous vraiment partager votre bureau ?"), ("Display", "Affichage"), ("Default View Style", "Style de vue par défaut"), ("Default Scroll Style", "Style de défilement par défaut"), - ("Default Image Quality", "Qualité d'image par défaut"), + ("Default Image Quality", "Qualité d’image par défaut"), ("Default Codec", "Codec par défaut"), ("Bitrate", "Débit"), ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Autres options par défaut"), - ("Voice call", "Appel voix"), + ("Voice call", "Appel vocal"), ("Text chat", "Conversation textuelle"), - ("Stop voice call", "Stopper l'appel voix"), - ("relay_hint_tip", "Il se peut qu'il ne doit pas possible de se connecter directement, vous pouvez essayer de vous connecter via un relais. \nEn outre, si vous souhaitez utiliser directement le relais, vous pouvez ajouter le suffixe \"/r\" à l'ID ou sélectionner l'option \"Toujours se connecter via le relais\" dans la fiche appareils distants."), + ("Stop voice call", "Terminer l’appel vocal"), + ("relay_hint_tip", "Il n’est pas toujours possible d’établir une connexion directe, mais une connexion via serveur relais est envisageable. En outre, si vous souhaitez utiliser un relais dès la première tentative, vous pouvez ajouter le suffixe « /r » à l’ID ou activer l’option « Forcer la connexion via relais » depuis la carte des sessions récentes, si elle s’y trouve."), ("Reconnect", "Se reconnecter"), ("Codec", "Codec"), ("Resolution", "Résolution"), - ("No transfers in progress", "Pas de transfert en cours"), + ("No transfers in progress", "Aucun transfert en cours"), ("Set one-time password length", "Définir la longueur du mot de passe à usage unique"), - ("RDP Settings", "Configuration RDP"), + ("RDP Settings", "Paramètres RDP"), ("Sort by", "Trier par"), ("New Connection", "Nouvelle connexion"), ("Restore", "Restaurer"), ("Minimize", "Minimiser"), ("Maximize", "Maximiser"), ("Your Device", "Votre appareil"), - ("empty_recent_tip", "Oups, pas de sessions récentes !\nIl est temps d'en prévoir une nouvelle."), - ("empty_favorite_tip", "Vous n'avez pas encore d'appareils distants favorits ?\nTrouvons quelqu'un avec qui vous connecter et ajoutez-les à vos favoris !"), - ("empty_lan_tip", "Oh non, il semble que nous n'ayons pas encore d'appareils réseau local découverts."), - ("empty_address_book_tip", "Ouh là là ! il semble qu'il n'y ait actuellement aucun appareil distant répertorié dans votre carnet d'adresses."), - ("eg: admin", "ex: admin"), - ("Empty Username", "Nom d'utilisation non spécifié"), - ("Empty Password", "Mot de passe non spécifié"), + ("empty_recent_tip", "Oups, aucune session récente !\nIl est l’heure d’en organiser une nouvelle."), + ("empty_favorite_tip", "Vous n’avez pas encore d’appareils distants favoris ?\nTrouvez quelqu’un avec qui vous connecter et ajoutez-le à vos favoris !"), + ("empty_lan_tip", "Oh non, il semble que nous n’avons pas encore découvert d’appareils sur le réseau local."), + ("empty_address_book_tip", "Mince, il n’y a actuellement aucun appareil distant répertorié dans votre carnet d’adresses."), + ("eg: admin", "ex : admin"), + ("Empty Username", "Nom d’utilisation non renseigné"), + ("Empty Password", "Mot de passe non renseigné"), ("Me", "Moi"), - ("identical_file_tip", "Ce fichier est identique à celui de l'appareil distant."), - ("show_monitors_tip", "Afficher les moniteurs dans la barre d'outils"), + ("identical_file_tip", "Ce fichier est identique à celui sur l’appareil distant."), + ("show_monitors_tip", "Afficher les écrans dans la barre d’outils"), ("View Mode", "Mode vue"), - ("login_linux_tip", "Se connecter au compte Linux distant"), + ("login_linux_tip", "Vous devez vous connecter au compte Linux distant pour établir une session de bureau X"), ("verify_rustdesk_password_tip", "Vérifier le mot de passe RustDesk"), ("remember_account_tip", "Se souvenir de ce compte"), - ("os_account_desk_tip", "Ce compte est utilisé pour se connecter au système d'exploitation distant et activer la session de bureau en mode sans affichage"), - ("OS Account", "Compte système d'exploitation"), + ("os_account_desk_tip", "Ce compte est utilisé pour se connecter au système d’exploitation distant et activer la session de bureau en mode sans affichage"), + ("OS Account", "Compte du système d’exploitation"), ("another_user_login_title_tip", "Un autre utilisateur est déjà connecté"), - ("another_user_login_text_tip", "Déconnexion"), + ("another_user_login_text_tip", "Déconnecter"), ("xorg_not_found_title_tip", "Xorg introuvable"), ("xorg_not_found_text_tip", "Veuillez installer Xorg"), - ("no_desktop_title_tip", "Aucun gestionaire de bureau n'est disponible"), - ("no_desktop_text_tip", "Veuillez installer le gestionaire de bureau GNOME"), - ("No need to elevate", "Pas besoin de permissions administrateur"), + ("no_desktop_title_tip", "Aucun environnement de bureau n’est disponible"), + ("no_desktop_text_tip", "Veuillez installer l’environnement de bureau GNOME"), + ("No need to elevate", "Élever les privilèges n’est pas nécessaire"), ("System Sound", "Son système"), ("Default", "Défaut"), ("New RDP", "Nouvel RDP"), - ("Fingerprint", "Empreinte digitale"), - ("Copy Fingerprint", "Copier empreinte digitale"), - ("no fingerprints", "Pas d'empreintes digitales"), - ("Select a peer", "Sélectionnez l'appareil distant"), - ("Select peers", "Sélectionnez des appareils distants"), + ("Fingerprint", "Empreinte numérique"), + ("Copy Fingerprint", "Copier l’empreinte numérique"), + ("no fingerprints", "Aucune empreinte numérique"), + ("Select a peer", "Sélectionnez l’appareil distant"), + ("Select peers", "Sélectionnez les appareils distants"), ("Plugins", "Plugins"), ("Uninstall", "Désinstaller"), - ("Update", "Mise à jour"), - ("Enable", "Activé"), - ("Disable", "Desactivé"), + ("Update", "Mettre à jour"), + ("Enable", "Activer"), + ("Disable", "Désactiver"), ("Options", "Options"), - ("resolution_original_tip", "Résolution d'origine"), - ("resolution_fit_local_tip", "Adapter la résolution local"), + ("resolution_original_tip", "Résolution d’origine"), + ("resolution_fit_local_tip", "Adapter à la résolution locale"), ("resolution_custom_tip", "Résolution personnalisée"), - ("Collapse toolbar", "Réduire la barre d'outils"), - ("Accept and Elevate", "Accepter et autoriser l'augmentation des privilèges"), - ("accept_and_elevate_btn_tooltip", "Accepter la connexion l'augmentation des privilèges UAC."), - ("clipboard_wait_response_timeout_tip", "Expiration du délai d'attente presse-papiers."), + ("Collapse toolbar", "Réduire la barre d’outils"), + ("Accept and Elevate", "Accepter et élever les privilèges"), + ("accept_and_elevate_btn_tooltip", "Accepter la connexion et élever les privilèges UAC."), + ("clipboard_wait_response_timeout_tip", "Expiration du délai d’attente du presse-papier."), ("Incoming connection", "Connexion entrante"), ("Outgoing connection", "Connexion sortante"), ("Exit", "Quitter"), ("Open", "Ouvrir"), - ("logout_tip", "Êtes-vous sûr de vouloir vous déconnecter ?"), + ("logout_tip", "Voulez-vous vraiment vous déconnecter ?"), ("Service", "Service"), - ("Start", "Lancer"), - ("Stop", "Stopper"), - ("exceed_max_devices", "Vous avez atteint le nombre maximal d'appareils gérés."), + ("Start", "Démarrer"), + ("Stop", "Arrêter"), + ("exceed_max_devices", "Vous avez atteint le nombre maximal d’appareils gérés."), ("Sync with recent sessions", "Synchroniser avec les sessions récentes"), ("Sort tags", "Trier les étiquettes"), - ("Open connection in new tab", "Ouvrir la connexion dans un nouvel onglet"), - ("Move tab to new window", "Déplacer l'onglet vers une nouvelle fenêtre"), - ("Can not be empty", "Ne peux pas être vide"), + ("Open connection in new tab", "Ouvrir les connexions dans un nouvel onglet"), + ("Move tab to new window", "Déplacer l’onglet vers une nouvelle fenêtre"), + ("Can not be empty", "Ne peut pas être vide"), ("Already exists", "Existe déjà"), - ("Change Password", "Changer le mot de passe"), + ("Change Password", "Modifier le mot de passe"), ("Refresh Password", "Actualiser le mot de passe"), ("ID", "ID"), ("Grid View", "Vue Grille"), ("List View", "Vue Liste"), ("Select", "Sélectionner"), - ("Toggle Tags", "Basculer vers les étiquettes"), - ("pull_ab_failed_tip", "Impossible d'actualiser le carnet d'adresses"), - ("push_ab_failed_tip", "Échec de la synchronisation du carnet d'adresses"), - ("synced_peer_readded_tip", "Les appareils qui étaient présents dans les sessions récentes seront synchronisés avec le carnet d'adresses."), + ("Toggle Tags", "Basculer les étiquettes"), + ("pull_ab_failed_tip", "Échec de l’actualisation du carnet d’adresses"), + ("push_ab_failed_tip", "Échec de la synchronisation du carnet d’adresses avec le serveur"), + ("synced_peer_readded_tip", "Les appareils qui étaient présents dans les sessions récentes seront synchronisés vers le carnet d’adresses."), ("Change Color", "Modifier la couleur"), - ("Primary Color", "Couleur primaire"), - ("HSV Color", "Couleur TSL"), - ("Installation Successful!", "Installation réussie !"), - ("Installation failed!", "Échec de l'installation !"), + ("Primary Color", "Couleur principale"), + ("HSV Color", "Couleur TSV"), + ("Installation Successful!", "Installation réussie !"), + ("Installation failed!", "Échec de l’installation !"), ("Reverse mouse wheel", "Inverser le sens de la molette de la souris"), - ("{} sessions", "{} sessions"), - ("scam_title", "Vous êtes peut-être victime d'une ESCROQUERIE !"), - ("scam_text1", "Si vous êtes au téléphone avec quelqu'un QUE VOUS NE CONNAISSEZ PAS et en qui VOUS N'AVEZ PAS CONFIANCE et qui vous a demandé d'utiliser RustDesk et de démarrer le service, ne le faites pas et raccrochez immédiatement."), - ("scam_text2", "Il s'agit probablement d'un escroc qui tente de vous voler de l'argent ou d'autres informations personnelles."), - ("Don't show again", "Ne plus montrer"), - ("I Agree", "J'accepte"), + ("{} sessions", "{} sessions"), + ("scam_title", "Vous êtes peut-être victime d’une ESCROQUERIE !"), + ("scam_text1", "Si vous êtes au téléphone avec quelqu’un QUE VOUS NE CONNAISSEZ PAS et en qui VOUS N’AVEZ PAS CONFIANCE et qui vous a demandé d’utiliser RustDesk et de démarrer le service, ne le faites pas et raccrochez immédiatement."), + ("scam_text2", "Il s’agit probablement d’un escroc qui tente de vous voler de l’argent ou d’autres informations personnelles."), + ("Don't show again", "Ne plus afficher"), + ("I Agree", "J’accepte"), ("Decline", "Refuser"), - ("Timeout in minutes", "Délai d'expiration en minutes"), - ("auto_disconnect_option_tip", "Fermer automatiquement les sessions entrantes en cas d'inactivité de l'utilisateur"), - ("Connection failed due to inactivity", "Déconnecté automatiquement pour cause d'inactivité"), + ("Timeout in minutes", "Délai d’expiration en minutes"), + ("auto_disconnect_option_tip", "Terminer automatiquement les sessions entrantes en cas d’inactivité de l’utilisateur"), + ("Connection failed due to inactivity", "Déconnecté automatiquement pour cause d’inactivité"), ("Check for software update on startup", "Vérifier la disponibilité des mises à jour au démarrage"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "Veuillez mettre à jour RustDesk Server Pro avec la version {} ou une version plus récente !"), - ("pull_group_failed_tip", "Échec de l'actualisation du groupe"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Veuillez mettre à jour RustDesk Server Pro vers la version {} ou une version ultérieure !"), + ("pull_group_failed_tip", "Échec de l’actualisation du groupe"), ("Filter by intersection", "Filtrer par intersection"), - ("Remove wallpaper during incoming sessions", "Supprimer le fond d'écran lors des sessions entrantes"), - ("Test", ""), - ("display_is_plugged_out_msg", "L'écran est débranché, passez au premier écran."), + ("Remove wallpaper during incoming sessions", "Cacher le fond d’écran lors des sessions entrantes"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "L’affichage est débranché, passez sur le premier affichage."), ("No displays", "Aucun affichage"), ("Open in new window", "Ouvrir dans une nouvelle fenêtre"), ("Show displays as individual windows", "Montrer les affichages sous forme de fenêtres individuelles"), - ("Use all my displays for the remote session", "Utiliser tous mes écrans pour la session à distance"), - ("selinux_tip", "SELinux est activé sur votre appareil, ce qui peut empêcher RustDesk de fonctionner correctement en tant que machine contrôlé."), - ("Change view", "Disposition d'affichage"), + ("Use all my displays for the remote session", "Utiliser tous mes affichages pour la session à distance"), + ("selinux_tip", "SELinux est activé sur votre appareil, ce qui peut empêcher RustDesk de fonctionner correctement sur la machine contrôlée."), + ("Change view", "Disposition"), ("Big tiles", "Grandes tuiles"), ("Small tiles", "Petites tuiles"), ("List", "Liste"), ("Virtual display", "Affichage virtuel"), - ("Plug out all", "Déconnecter tout"), + ("Plug out all", "Tout débrancher"), ("True color (4:4:4)", "Couleur réelle (4:4:4)"), - ("Enable blocking user input", "Activer le blocage des entrées utilisateur"), - ("id_input_tip", "Vous pouvez saisir un ID, une adresse IP directe ou un nom de domaine avec un port (:).\nSi vous souhaitez accéder à un appareil sur un autre serveur, veuillez ajouter l'adresse du serveur (?key=), par exemple,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi vous souhaitez accéder à un appareil sur un serveur public, veuillez saisir \"@public\" , la clé n'est pas nécessaire pour le serveur public"), - ("privacy_mode_impl_mag_tip", "Mode 1"), - ("privacy_mode_impl_virtual_display_tip", "Mode 2"), - ("Enter privacy mode", "Passer en mode confidentialité"), - ("Exit privacy mode", "Quitter le mode confidentialité"), - ("idd_not_support_under_win10_2004_tip", "Le pilote d'affichage indirect n'est pas pris en charge. Windows 10, version 2004 ou plus récente est requise."), - ("input_source_1_tip", "Source entrée 1"), - ("input_source_2_tip", "Source entrée 2"), - ("Swap control-command key", "Échanger la touche de controle-commande"), - ("swap-left-right-mouse", "Intervertir le bouton gauche et droit de la souris"), - ("2FA code", "code 2FA"), + ("Enable blocking user input", "Activer le blocage des entrées de l’utilisateur"), + ("id_input_tip", "Vous pouvez saisir un ID, une adresse IP ou un nom de domaine suivi d’un port (:).\nSi vous souhaitez accéder à un appareil sur un autre serveur, veuillez ajouter l’adresse du serveur (@?key=), par exemple :\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi vous souhaitez accéder à un appareil sur un serveur public, veuillez saisir « @public » (la clé n’est pas nécessaire pour le serveur public).\n\nSi vous souhaitez forcer l’utilisation d’une connexion via relais dès la première tentative, ajoutez « /r » après l’ID, par exemple : « 9123456234/r »."), + ("privacy_mode_impl_mag_tip", "Mode 1"), + ("privacy_mode_impl_virtual_display_tip", "Mode 2"), + ("Enter privacy mode", "Entrer en mode de confidentialité"), + ("Exit privacy mode", "Quitter le mode de confidentialité"), + ("idd_not_support_under_win10_2004_tip", "Le pilote d’affichage indirect n’est pas pris en charge. Windows 10 version 2004 ou ultérieure est requis."), + ("input_source_1_tip", "Entrée source 1"), + ("input_source_2_tip", "Entrée source 2"), + ("Swap control-command key", "Intervertir la touche contrôle-commande"), + ("swap-left-right-mouse", "Intervertir les boutons gauche et droit de la souris"), + ("2FA code", "Code 2FA"), ("More", "Plus"), - ("enable-2fa-title", "Activer l'authentification à double facteur"), - ("enable-2fa-desc", "Veuillez configurer votre authentificateur maintenant. Vous pouvez utiliser une application d’authentification telle qu’Authy, Microsoft ou Google Authenticator sur votre téléphone ou votre ordinateur de bureau.nnScannez le code QR avec votre application et entrez le code affiché par votre application pour activer l’authentification à deux facteurs."), - ("wrong-2fa-code", "Impossible de vérifier le code. Vérifiez que le code et les paramètres d’heure locale sont corrects"), + ("enable-2fa-title", "Activer l’authentification à deux facteurs"), + ("enable-2fa-desc", "Veuillez maintenant configurer votre authentificateur. Vous pouvez utiliser une application d’authentification telle qu’Authy, Microsoft ou Google Authenticator sur votre téléphone ou votre ordinateur.\n\nScannez le code QR avec votre application puis saisissez le code affiché par votre application afin d’activer l’authentification à deux facteurs."), + ("wrong-2fa-code", "Impossible de vérifier le code. Vérifiez l’exactitude du code saisi ainsi que des paramètres d’heure locale"), ("enter-2fa-title", "Authentification à deux facteurs"), - ("Email verification code must be 6 characters.", "Le code de vérification email doit comporter 6 caractères"), - ("2FA code must be 6 digits.", "le code 2FA doit comporter 6 chiffres"), - ("Multiple Windows sessions found", "Plusieurs sessions Windows trouvées"), - ("Please select the session you want to connect to", "Merci de sélectionner la session Windows à laquelle vous voulez vous connecter"), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", "Il s’agit d’une édition personnalisée.\nVous pouvez vous connecter à d’autres appareils, mais les autres appareils ne peuvent pas se connecter à votre appareil."), - ("preset_password_warning", "Cette édition personnalisée est livrée avec un mot de passe prédéfini. Toute personne connaissant ce mot de passe pourrait prendre le contrôle total de votre appareil. Si vous ne vous y attendiez pas, désinstallez immédiatement le logiciel."), + ("Email verification code must be 6 characters.", "Le code de vérification de l’adresse électronique doit être composé de 6 caractères."), + ("2FA code must be 6 digits.", "Le code 2FA doit être composé de 6 chiffres."), + ("Multiple Windows sessions found", "Plusieurs sessions Windows ont été trouvées"), + ("Please select the session you want to connect to", "Veuillez sélectionner la session à laquelle vous souhaitez vous connecter"), + ("powered_by_me", "Utilise la technologie RustDesk"), + ("outgoing_only_desk_tip", "Vous utilisez une version personnalisée.\nVous pouvez vous connecter à d’autres appareils, mais les autres appareils ne peuvent pas se connecter au vôtre."), + ("preset_password_warning", "Cette version personnalisée est livrée avec un mot de passe prédéfini. Toute personne connaissant ce mot de passe pourrait prendre le contrôle total de votre appareil. Si vous ne vous y attendiez pas, désinstallez immédiatement le logiciel."), ("Security Alert", "Alerte de sécurité"), - ("My address book", "Mon carnet d'adresse"), + ("My address book", "Mon carnet d’adresses"), ("Personal", "Personnel"), ("Owner", "Propriétaire"), ("Set shared password", "Définir le mot de passe partagé"), ("Exist in", "Existe dans"), - ("Read-only", "Lecture-seule"), + ("Read-only", "Lecture seule"), ("Read/Write", "Lecture/Écriture"), - ("Full Control", "Control Total"), + ("Full Control", "Contrôle complet"), ("share_warning_tip", "Les champs ci-dessus sont partagés et visibles par les autres."), ("Everyone", "Tout le monde"), - ("ab_web_console_tip", "Plus sur la console Web"), + ("ab_web_console_tip", "Plus sur la console web"), ("allow-only-conn-window-open-tip", "N’autoriser la connexion que si la fenêtre RustDesk est ouverte"), - ("no_need_privacy_mode_no_physical_displays_tip", "Pas d’affichage physique, pas besoin d’utiliser le mode confidentialité."), + ("no_need_privacy_mode_no_physical_displays_tip", "Aucun affichage physique ; l’utilisation du mode de confidentialité n’est pas nécessaire."), ("Follow remote cursor", "Suivre le curseur distant"), - ("Follow remote window focus", ""), + ("Follow remote window focus", "Suivre la focalisation de fenêtre distante"), ("default_proxy_tip", "Le protocole et le port par défaut sont Socks5 et 1080"), ("no_audio_input_device_tip", "Aucun périphérique d’entrée audio trouvé."), - ("Incoming", "Entrant"), - ("Outgoing", "Sortant"), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", "Une nouvelle demande d’appel vocal a été reçue. Si vous acceptez, l’audio passera à la communication vocale."), - ("texture_render_tip", "Utilisez le rendu des textures pour rendre les images plus fluides."), + ("Incoming", "Entrantes"), + ("Outgoing", "Sortantes"), + ("Clear Wayland screen selection", "Effacer la sélection d’écran Wayland"), + ("clear_Wayland_screen_selection_tip", "Une fois la sélection d’écran effacée, vous pourrez resélectionner l’écran à partager."), + ("confirm_clear_Wayland_screen_selection_tip", "Voulez-vous vraiment effacer la sélection d’écran Wayland ?"), + ("android_new_voice_call_tip", "Une nouvelle demande d’appel vocal a été reçue. Si vous acceptez, l’audio passera sur la communication vocale."), + ("texture_render_tip", "Utiliser le rendu de texture afin de lisser les images. Désactiver cette option permet de résoudre certains problèmes de rendu."), ("Use texture rendering", "Utiliser le rendu de texture"), - ("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", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), - ("Accessible devices", ""), + ("Floating window", "Fenêtre flottante"), + ("floating_window_tip", "Aide à maintenir le service en arrière-plan"), + ("Keep screen on", "Maintenir l’écran allumé"), + ("Never", "Jamais"), + ("During controlled", "Lorsque l’appareil est contrôlé"), + ("During service is on", "Lorsque le service est actif"), + ("Capture screen using DirectX", "Utiliser DirectX pour capturer l’écran"), + ("Back", "Retour"), + ("Apps", "Applis"), + ("Volume up", "Volume haut"), + ("Volume down", "Volume bas"), + ("Power", "Marche/Arrêt"), + ("Telegram bot", "Bot Telegram"), + ("enable-bot-tip", "Activer cette fonctionnalité vous permet de recevoir le code 2FA depuis votre bot. Peut également servir de notification de connexion."), + ("enable-bot-desc", "1. Entamez une discussion avec @BotFather.\n2. Envoyez-lui la commande « newbot ». Vous recevrez un jeton suite à cette étape.\n3. Entamez une discussion avec votre bot nouvellement créé. Envoyez-lui un message commençant par une barre oblique (« / ») tel que « /hello » afin de l’activer.\n"), + ("cancel-2fa-confirm-tip", "Voulez-vous vraiment désactiver l’authentication à deux facteurs ?"), + ("cancel-bot-confirm-tip", "Voulez-vous vraiment désactiver le bot Telegram ?"), + ("About RustDesk", "À propos de RustDesk"), + ("Send clipboard keystrokes", "Taper le contenu du presse-papier"), + ("network_error_tip", "Veuillez vérifier votre connexion réseau puis réessayer."), + ("Unlock with PIN", "Déverrouiller par code PIN"), + ("Requires at least {} characters", "Requiert un minimum de {} caractères"), + ("Wrong PIN", "Code PIN erroné"), + ("Set PIN", "Définir le code PIN"), + ("Enable trusted devices", "Activer les appareils de confiance"), + ("Manage trusted devices", "Gérer les appareils de confiance"), + ("Platform", "Plateforme"), + ("Days remaining", "Jours restants"), + ("enable-trusted-devices-tip", "Ne pas demander de code 2FA sur les appareils de confiance"), + ("Parent directory", "Répertoire parent"), + ("Resume", "Reprendre"), + ("Invalid file name", "Nom de fichier non valide"), + ("one-way-file-transfer-tip", "Le transfert de fichiers à sens unique est activé côté appareil contrôlé."), + ("Authentication Required", "Authentication requise"), + ("Authenticate", "Authentifier"), + ("web_id_input_tip", "Vous pouvez saisir un ID sur le même serveur ; le client web ne prend pas en charge l’accès par adresse IP.\nSi vous souhaitez accéder à un appareil sur un autre serveur, veuillez ajouter l’adresse du serveur (@?key=), par exemple :\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi vous souhaitez accéder à un appareil sur un serveur public, veuillez saisir « @public » (la clé n’est pas nécessaire pour le serveur public)."), + ("Download", "Télécharger"), + ("Upload folder", "Téléverser le dossier"), + ("Upload files", "Téléverser les fichiers"), + ("Clipboard is synchronized", "Le presse-papier est synchronisé"), + ("Update client clipboard", "Actualiser le presse-papier du client"), + ("Untagged", "Sans étiquette"), + ("new-version-of-{}-tip", "Une nouvelle version de {} est disponible"), + ("Accessible devices", "Appareils accessibles"), ].iter().cloned().collect(); } From 0bda90f8fb7cb628f8c9df36d951958b098ed062 Mon Sep 17 00:00:00 2001 From: marboroman Date: Mon, 3 Mar 2025 09:55:11 +0300 Subject: [PATCH 123/506] Update ru.rs (#10978) update wrong translation for privacy mode activation --- 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 5ba372b408c..1b02cad77d5 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -567,8 +567,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("id_input_tip", "Можно ввести идентификатор, прямой 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"), From 32b77f8968dee76562711c38954aed9109e38b33 Mon Sep 17 00:00:00 2001 From: marboroman Date: Mon, 3 Mar 2025 09:55:21 +0300 Subject: [PATCH 124/506] Update ru.rs (#10979) Made long translation short to fit in user interface. --- 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 1b02cad77d5..4e46def5361 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Служба запущена"), ("Service is not running", "Служба не запущена"), ("not_ready_status", "Не подключено. Проверьте соединение."), - ("Control Remote Desktop", "Управление удалённым рабочим столом"), + ("Control Remote Desktop", "Новое соединение"), ("Transfer file", "Передать файлы"), ("Connect", "Подключиться"), ("Recent sessions", "Последние сеансы"), From 6600c8c648022dd4e901f553a50562bae17ce33f Mon Sep 17 00:00:00 2001 From: marboroman Date: Mon, 3 Mar 2025 09:55:32 +0300 Subject: [PATCH 125/506] Update ru.rs (#10984) Changed to the translation which makes sense. --- 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 4e46def5361..e0c8eaeb147 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -299,7 +299,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unsupported", "Не поддерживается"), ("Peer denied", "Отклонено удалённым узлом"), ("Please install plugins", "Установите плагины"), - ("Peer exit", "Удалённый узел отключён"), + ("Peer exit", "Отключено пользователем"), ("Failed to turn off", "Невозможно отключить"), ("Turned off", "Отключён"), ("Language", "Язык"), From 7305b6bd1c815429dbcdd6c8dd57a5ca16b01375 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 3 Mar 2025 19:06:01 +0800 Subject: [PATCH 126/506] https://github.com/rustdesk/rustdesk/discussions/937#discussioncomment-12373814 try to support citrix session --- src/platform/windows.cc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/platform/windows.cc b/src/platform/windows.cc index 04095a2d63f..cbf80c49769 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -543,6 +543,9 @@ extern "C" DWORD count; auto rdp = "rdp"; auto nrdp = strlen(rdp); + // https://github.com/rustdesk/rustdesk/discussions/937#discussioncomment-12373814 citrix session + auto ica = "ica"; + auto nica = strlen(ica); if (WTSEnumerateSessionsA(WTS_CURRENT_SERVER_HANDLE, NULL, 1, &pInfos, &count)) { for (DWORD i = 0; i < count; i++) @@ -558,7 +561,7 @@ extern "C" WTSFreeMemory(pInfos); return id; } - if (!strnicmp(info.pWinStationName, rdp, nrdp)) + if (!strnicmp(info.pWinStationName, rdp, nrdp) || !strnicmp(info.pWinStationName, ica, nica)) { rdp_or_console = info.SessionId; } @@ -614,6 +617,8 @@ extern "C" auto info = pInfos[i]; auto rdp = "rdp"; auto nrdp = strlen(rdp); + auto ica = "ica"; + auto nica = strlen(ica); if (info.State == WTSActive) { if (info.pWinStationName == NULL) continue; @@ -626,6 +631,9 @@ extern "C" else if (include_rdp && !strnicmp(info.pWinStationName, rdp, nrdp)) { sessionIds.push_back(std::wstring(L"RDP:") + std::to_wstring(info.SessionId)); } + else if (include_rdp && !strnicmp(info.pWinStationName, ica, nica)) { + sessionIds.push_back(std::wstring(L"ICA:") + std::to_wstring(info.SessionId)); + } } } WTSFreeMemory(pInfos); From 171d178b09ce887a3b55e58df389174ca743b471 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 4 Mar 2025 14:11:18 +0800 Subject: [PATCH 127/506] make errorText of DialogTextField selectable (#11013) Signed-off-by: 21pages --- flutter/lib/common/widgets/dialog.dart | 50 ++++++++++++++++---------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 135d28c1d59..345a140ad16 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -412,24 +412,38 @@ class DialogTextField extends StatelessWidget { return Row( children: [ Expanded( - child: TextField( - decoration: InputDecoration( - labelText: title, - hintText: hintText, - prefixIcon: prefixIcon, - suffixIcon: suffixIcon, - helperText: helperText, - helperMaxLines: 8, - errorText: errorText, - errorMaxLines: 8, - ), - controller: controller, - focusNode: focusNode, - autofocus: true, - obscureText: obscureText, - keyboardType: keyboardType, - inputFormatters: inputFormatters, - maxLength: maxLength, + child: Column( + children: [ + TextField( + decoration: InputDecoration( + labelText: title, + hintText: hintText, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + helperText: helperText, + helperMaxLines: 8, + ), + controller: controller, + focusNode: focusNode, + autofocus: true, + obscureText: obscureText, + keyboardType: keyboardType, + inputFormatters: inputFormatters, + maxLength: maxLength, + ), + if (errorText != null) + Align( + alignment: Alignment.centerLeft, + child: SelectableText( + errorText!, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + textAlign: TextAlign.left, + ).paddingOnly(top: 8, left: 12), + ), + ], ).workaroundFreezeLinuxMint(), ), ], From 8f68861920287947c2f1e6eb8fa2a6470528aae9 Mon Sep 17 00:00:00 2001 From: Yuni <60534152+yeonhee7935@users.noreply.github.com> Date: Tue, 4 Mar 2025 18:37:54 +0900 Subject: [PATCH 128/506] Update CONTRIBUTING-KR.md (#11016) * Update CONTRIBUTING-KR.md Signed-off-by: yeonhee7935 * Update CONTRIBUTING-KR.md Signed-off-by: yeonhee7935 --------- Signed-off-by: yeonhee7935 --- docs/CONTRIBUTING-KR.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/CONTRIBUTING-KR.md diff --git a/docs/CONTRIBUTING-KR.md b/docs/CONTRIBUTING-KR.md new file mode 100644 index 00000000000..0af10201595 --- /dev/null +++ b/docs/CONTRIBUTING-KR.md @@ -0,0 +1,40 @@ +# RustDesk 기여 가이드라인 + +RustDesk는 모든 사람의 기여를 환영합니다. 만약 RustDesk에 기여하고 싶다면 아래 가이드를 참고해주세요: + +## 기여 방식 + +RustDesk 또는 종속성에 대한 기여는 GitHub Pull Request 형태로 이루어져야 합니다. +모든 Pull Request는 코어 기여자가 검토하며, 메인 저장소에 반영되거나 필요한 수정 사항에 대한 피드백을 받습니다. +모든 기여는 이 형식을 따라야 합니다. + +특정 이슈에 작업하고 싶다면, 먼저 GitHub 이슈에 댓글을 달아 작업하겠다고 알려주세요. +이는 동일한 작업에 대해 중복 기여가 발생하는 것을 방지하기 위함입니다. + +## Pull Request Checklist + +- master 브랜치에서 새 브랜치를 생성하고 작업하세요.
    +필요한 경우 PR 제출 전에 최신 master 브랜치에 리베이스(rebase)하세요.
    +충돌이 발생하면 기여자가 직접 해결해야 합니다. + +- 커밋은 가능한 한 작고 독립적인 단위로 작성하세요.
    +각 커밋은 독립적으로 빌드와 테스트를 통과해야 합니다. + +- 커밋에는 반드시 Developer Certificate of Origin (http://developercertificate.org) 서명이 포함되어야 합니다.
    +이는 기여자(및 소속된 고용주가 있을 경우) 가 [프로젝트 라이선스](../LICENCE) 에 동의함을 나타냅니다.
    +Git에서는 `git commit` 명령어에 `-s` 옵션을 사용해 서명을 추가할 수 있습니다. + +- PR이 검토되지 않거나 특정 리뷰어가 필요하면, +
    PR이나 댓글에서 리뷰어를 태그하거나 [이메일](mailto:info@rustdesk.com)로 리뷰를 요청할 수 있습니다. + +- 수정된 버그나 추가된 기능과 관련된 테스트 코드를 포함해주세요. + +Git 사용에 대한 자세한 내용은 [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow)을 참조하세요. + +## 행동 강령 + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## 커뮤니케이션 + +RustDesk 기여자들은 [Discord](https://discord.gg/nDceKgxnkV)에서 활동하고 있습니다. From 6946b863f7f6c4687c93368d6f162e401e484d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Be=C3=A0?= Date: Wed, 5 Mar 2025 09:14:42 +0100 Subject: [PATCH 129/506] Update ca.rs (#11024) Update catalan localization --- src/lang/ca.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index ce8aa9fcdf8..e5b3ea1592b 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -649,13 +649,13 @@ 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", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), + ("Download", "Descarrega"), + ("Upload folder", "Puja una carpeta"), + ("Upload files", "Puja fitxers"), + ("Clipboard is synchronized", "El porta-retalls està sincronitzat"), + ("Update client clipboard", "Actualitza el porta-retalls del client"), + ("Untagged", "Sense etiquetar"), ("new-version-of-{}-tip", ""), - ("Accessible devices", ""), + ("Accessible devices", "Dispositius accessibles"), ].iter().cloned().collect(); } From 561bc18f49b0f18abc69eab39b55ed9ffcd94c0e Mon Sep 17 00:00:00 2001 From: zuiyu <1542844298@qq.com> Date: Thu, 6 Mar 2025 09:49:04 +0800 Subject: [PATCH 130/506] Fix unused import with dxgiformat (#11032) --- libs/scrap/src/dxgi/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 9220fa60593..a9d1af85c0e 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -7,7 +7,6 @@ use winapi::{ shared::{ dxgi::*, dxgi1_2::*, - dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM, dxgitype::*, minwindef::{DWORD, FALSE, TRUE, UINT}, ntdef::LONG, @@ -118,6 +117,7 @@ impl Capturer { } else { hres } + // NVFBC(NVIDIA Capture SDK) which xpra used already deprecated, https://developer.nvidia.com/capture-sdk // also try high version DXGI for better performance, e.g. @@ -129,6 +129,8 @@ impl Capturer { // can help us update screen incrementally /* // not supported on my PC, try in the future + use winapi::shared::dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM; + let format : Vec = vec![DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE]; (*display.inner).DuplicateOutput1( device as *mut _, From 11fed81c4d97b6ff76431cfcb3c35ed41dcefdc9 Mon Sep 17 00:00:00 2001 From: Mani Ka Date: Wed, 5 Mar 2025 17:49:25 -0800 Subject: [PATCH 131/506] Update reference to flutter web UI (#11029) - the src js web UI has moved to directory v1 in flutter/web/js as v2 is under development . The link in README.md is returning 404 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 912c73a7df6..021338d6db0 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ Please ensure that you are running these commands from the root of the RustDesk - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile -- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript for Flutter web client > [!Caution] > **Misuse Disclaimer:**
    From c95aaf563e908be5bf017f74bf74c0bb1b7b1c04 Mon Sep 17 00:00:00 2001 From: ANB5Dev <90337467+ANB5Dev@users.noreply.github.com> Date: Thu, 6 Mar 2025 02:49:44 +0100 Subject: [PATCH 132/506] fixed Dutch names for macOS permissions (#11020) --- 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 425f0b5e849..529f47257ed 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -150,8 +150,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to download", "Klik om te downloaden"), ("Click to update", "Klik om bij te werken"), ("Configure", "Configureren"), - ("config_acc", "Om uw bureaublad op afstand te kunnen bedienen, moet u RustDesk toestemming voor \"toegankelijkheid\" geven."), - ("config_screen", "Om toegang te krijgen tot het externe bureaublad, moet u RustDesk toestemming voor \"schermregistratie\" geven."), + ("config_acc", "Om uw apparaat op afstand te kunnen bedienen, moet u RustDesk toestemming voor Toegankelijkheid geven."), + ("config_screen", "Om uw apparaat op afstand te kunnen bedienen, moet u RustDesk toestemming voor Schermopname geven."), ("Installing ...", "Installeren ..."), ("Install", "Installeer"), ("Installation", "Installatie"), @@ -412,8 +412,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Selecteer lokaal toetsenbord"), ("software_render_tip", "Als u een NVIDIA grafische kaart hebt en het externe venster sluit onmiddellijk na verbinding, kan het helpen om het nieuwe stuurprogramma te installeren en te kiezen voor software rendering. Een software herstart is vereist."), ("Always use software rendering", "Gebruik altijd software rendering"), - ("config_input", "Om het externe bureaublad vanaf het toetsenbord te kunnen bedienen, moet u RustDesk de rechten \"Invoer vastleggen\" geven."), - ("config_microphone", "Om op afstand te kunnen chatten, moet u RustDesk 'Audio opnemen' rechten geven."), + ("config_input", "Om een extern apparaat met uw toetsenbord te kunnen bedienen, moet u RustDesk toestemming voor Invoer Vastleggen geven."), + ("config_microphone", "Om te kunnen chatten moet u RustDesk toestemming voor Microfoon geven."), ("request_elevation_tip", "U kunt ook meer rechten vragen als iemand aan de andere kant aanwezig is."), ("Wait", "Wacht"), ("Elevation Error", "Verhogingsfout"), From f1329ca69eaec808758ef99bdff9f53f973336fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 09:21:40 +0800 Subject: [PATCH 133/506] Git submodule: Bump libs/hbb_common from `7cf11f7` to `83419b6` (#11042) Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `7cf11f7` to `83419b6`. - [Commits](https://github.com/rustdesk/hbb_common/compare/7cf11f7b771e27ecbd14fd1dd0ced55a64f40eb5...83419b6549636ee39dacef7776c473f5802e08d6) --- updated-dependencies: - dependency-name: libs/hbb_common dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index 7cf11f7b771..83419b65496 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 7cf11f7b771e27ecbd14fd1dd0ced55a64f40eb5 +Subproject commit 83419b6549636ee39dacef7776c473f5802e08d6 From 4ff75412c3ea9b1943dbaf34822f87d66b8b9870 Mon Sep 17 00:00:00 2001 From: asereze Date: Fri, 7 Mar 2025 14:24:45 +0100 Subject: [PATCH 134/506] Update sc.rs (#11046) --- src/lang/sc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 4953fe85b38..bb49857aff1 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -548,7 +548,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("upgrade_rustdesk_server_pro_to_{}_tip", "Atualiza RustDesk Server Pro a sa versione {} o prus noa!"), ("pull_group_failed_tip", "Non faghet a annoare su grupu"), ("Filter by intersection", "Filtra pro rugrada"), - ("Remove wallpaper during incoming sessions", "Boga s'isfundu durante sas essiones in intrada"), + ("Remove wallpaper during incoming sessions", "Boga s'isfundu durante sas sessiones in intrada"), ("Test", "Proa"), ("display_is_plugged_out_msg", "S'ischermu est iscollegadu, colo a su primu ischermu."), ("No displays", "Perunu ischermu"), From cef41759619804a50375f1f4e832a71fb9db7dde Mon Sep 17 00:00:00 2001 From: Madis Otenurm Date: Sat, 8 Mar 2025 10:38:58 +0100 Subject: [PATCH 135/506] Update et.rs (#11054) * Update et.rs * Update et.rs Added more missing translations * Update et.rs * Update et.rs * Update et.rs --- src/lang/et.rs | 918 ++++++++++++++++++++++++------------------------- 1 file changed, 459 insertions(+), 459 deletions(-) diff --git a/src/lang/et.rs b/src/lang/et.rs index d5c47155d89..e40a2a477f2 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -1,254 +1,254 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", ""), - ("Your Desktop", ""), + ("Status", "Olek"), + ("Your Desktop", "Sinu töölaud"), ("desk_tip", "Sinu töölauale saab selle ID ja parooliga ligi pääseda."), - ("Password", ""), - ("Ready", ""), - ("Established", ""), + ("Password", "Parool"), + ("Ready", "Valmis"), + ("Established", "Ühendus loodud"), ("connecting_status", "RustDeski võrguga ühendumine..."), - ("Enable service", ""), - ("Start service", ""), - ("Service is running", ""), - ("Service is not running", ""), + ("Enable service", "Luba teenus"), + ("Start service", "Käivita teenus"), + ("Service is running", "Teenus töötab"), + ("Service is not running", "Teenus ei tööta"), ("not_ready_status", "Pole valmis. Palun kontrolli oma ühendust"), - ("Control Remote Desktop", ""), - ("Transfer file", ""), - ("Connect", ""), - ("Recent sessions", ""), - ("Address book", ""), - ("Confirmation", ""), - ("TCP tunneling", ""), - ("Remove", ""), - ("Refresh random password", ""), - ("Set your own password", ""), - ("Enable keyboard/mouse", ""), - ("Enable clipboard", ""), - ("Enable file transfer", ""), - ("Enable TCP tunneling", ""), - ("IP Whitelisting", ""), + ("Control Remote Desktop", "Juhi kaugtöölauda"), + ("Transfer file", "Edasta fail"), + ("Connect", "Ühenda"), + ("Recent sessions", "Viimatised seansid"), + ("Address book", "Aadressiraamat"), + ("Confirmation", "Kinnitus"), + ("TCP tunneling", "TCP-tunneldamine"), + ("Remove", "Eemalda"), + ("Refresh random password", "Värskenda juhuslik parool"), + ("Set your own password", "Määra oma parool"), + ("Enable keyboard/mouse", "Luba klaviatuur/hiir"), + ("Enable clipboard", "Luba lõikelaud"), + ("Enable file transfer", "Luba failiedastus"), + ("Enable TCP tunneling", "Luba TCP-tunneldamine"), + ("IP Whitelisting", "IP lubamisloend"), ("ID/Relay Server", "ID-/releeserver"), - ("Import server config", ""), - ("Export Server Config", ""), - ("Import server configuration successfully", ""), - ("Export server configuration successfully", ""), - ("Invalid server configuration", ""), - ("Clipboard is empty", ""), - ("Stop service", ""), - ("Change ID", ""), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), - ("id_change_tip", "Lubatud on vaid a-z, A-Z, 0-9, - (dash) ja _ (alakriips) tähemärgid. Esimene täht peab olema a-z või A-Z. Pikkus vahemikus 6-16."), - ("Website", ""), - ("About", ""), + ("Import server config", "Impordi serveriseadistus"), + ("Export Server Config", "Ekspordi serveriseadistus"), + ("Import server configuration successfully", "Serveriseadistus edukalt imporditud"), + ("Export server configuration successfully", "Serveriseadistus edukalt eksporditud"), + ("Invalid server configuration", "Sobimatu serveriseadistus"), + ("Clipboard is empty", "Lõikelaud on tühi"), + ("Stop service", "Peata teenus"), + ("Change ID", "Muuda ID-d"), + ("Your new ID", "Sinu uus ID"), + ("length %min% to %max%", "pikkus %min%-%max%"), + ("starts with a letter", "algab tähega"), + ("allowed characters", "lubatud tähemärgid"), + ("id_change_tip", "Lubatud on vaid a-z, A-Z, 0-9, - (kriips) ja _ (alakriips) tähemärgid. Esimene täht peab olema a-z või A-Z. Pikkus vahemikus 6-16."), + ("Website", "Veebileht"), + ("About", "Meist"), ("Slogan_tip", "Loodud südamega selles kaootilises maailmas!"), - ("Privacy Statement", ""), - ("Mute", ""), + ("Privacy Statement", "Privaatsusteatis"), + ("Mute", "Hääletu"), ("Build Date", "Ehituskuupäev"), - ("Version", ""), - ("Home", ""), + ("Version", "Versioon"), + ("Home", "Kodu"), ("Audio Input", "Helisisend"), - ("Enhancements", ""), + ("Enhancements", "Täiendused"), ("Hardware Codec", "Riistvarakoodek"), - ("Adaptive bitrate", ""), + ("Adaptive bitrate", "Kohanduv bitikiirus"), ("ID Server", "ID-server"), ("Relay Server", "Releeserver"), ("API Server", "Rakendusliidese server"), ("invalid_http", "peab algama: http:// või https://"), - ("Invalid IP", ""), - ("Invalid format", ""), + ("Invalid IP", "Sobimatu IP"), + ("Invalid format", "Sobimatu vorming"), ("server_not_support", "Pole veel serveri poolt toetatud"), - ("Not available", ""), - ("Too frequent", ""), - ("Cancel", ""), - ("Skip", ""), - ("Close", ""), - ("Retry", ""), - ("OK", ""), + ("Not available", "Pole saadaval"), + ("Too frequent", "Liiga sagedane"), + ("Cancel", "Tühista"), + ("Skip", "Jäta vahele"), + ("Close", "Sulge"), + ("Retry", "Proovi uuesti"), + ("OK", "OK"), ("Password Required", "Parool on nõutud"), - ("Please enter your password", ""), - ("Remember password", ""), + ("Please enter your password", "Palun sisesta oma parool"), + ("Remember password", "Jäta parool meelde"), ("Wrong Password", "Vale parool"), - ("Do you want to enter again?", ""), + ("Do you want to enter again?", "Kas soovid uuesti sisestada?"), ("Connection Error", "Ühenduse viga"), - ("Error", ""), - ("Reset by the peer", ""), - ("Connecting...", ""), - ("Connection in progress. Please wait.", ""), - ("Please try 1 minute later", ""), + ("Error", "Viga"), + ("Reset by the peer", "Partneri poolt lähtestatud"), + ("Connecting...", "Ühendamine..."), + ("Connection in progress. Please wait.", "Ühendus on käimas. Palun oota."), + ("Please try 1 minute later", "Palun proovi 1 minuti pärast"), ("Login Error", "Sisselogimise viga"), - ("Successful", ""), - ("Connected, waiting for image...", ""), - ("Name", ""), - ("Type", ""), - ("Modified", ""), - ("Size", ""), + ("Successful", "Edukas"), + ("Connected, waiting for image...", "Ühendatud, pildi ootamine..."), + ("Name", "Nimi"), + ("Type", "Tüüp"), + ("Modified", "Muudetud"), + ("Size", "Suurus"), ("Show Hidden Files", "Kuva peidetud faile"), - ("Receive", ""), - ("Send", ""), + ("Receive", "Võta vastu"), + ("Send", "Saada"), ("Refresh File", "Värskenda faili"), - ("Local", ""), - ("Remote", ""), + ("Local", "Kohalik"), + ("Remote", "Kaugjuhitav"), ("Remote Computer", "Kaugarvuti"), ("Local Computer", "Kohalik arvuti"), ("Confirm Delete", "Kinnita kustutamist"), - ("Delete", ""), - ("Properties", ""), + ("Delete", "Kustuta"), + ("Properties", "Atribuudid"), ("Multi Select", "Mitmikvalik"), ("Select All", "Vali kõik"), ("Unselect All", "Tühista kõigi valik"), ("Empty Directory", "Tühi kaust"), - ("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!", ""), - ("Deleting", ""), - ("files", ""), - ("Waiting", ""), - ("Finished", ""), - ("Speed", ""), + ("Not an empty directory", "Pole tühi kaust"), + ("Are you sure you want to delete this file?", "Kas soovid kindlasti selle faili eemaldada?"), + ("Are you sure you want to delete this empty directory?", "Kas soovid kindlasti selle tühja kausta eemaldada?"), + ("Are you sure you want to delete the file of this directory?", "Kas soovid kindlasti selle kausta faili eemaldada?"), + ("Do this for all conflicts", "Tee see kõigi konfliktide puhul"), + ("This is irreversible!", "See on pöördumatu!"), + ("Deleting", "Kustutamine"), + ("files", "failid"), + ("Waiting", "Ootamine"), + ("Finished", "Lõpetatud"), + ("Speed", "Kiirus"), ("Custom Image Quality", "Kohandatud pildikvaliteet"), - ("Privacy mode", ""), - ("Block user input", ""), - ("Unblock user input", ""), + ("Privacy mode", "Privaatsusrežiim"), + ("Block user input", "Blokeeri kasutajasisend"), + ("Unblock user input", "Eemalda kasutajasisendi blokeering"), ("Adjust Window", "Kohanda akent"), - ("Original", ""), - ("Shrink", ""), - ("Stretch", ""), - ("Scrollbar", ""), - ("ScrollAuto", ""), - ("Good image quality", ""), - ("Balanced", ""), - ("Optimize reaction time", ""), - ("Custom", ""), - ("Show remote cursor", ""), - ("Show quality monitor", ""), - ("Disable clipboard", ""), - ("Lock after session end", ""), - ("Insert Ctrl + Alt + Del", ""), + ("Original", "Algne"), + ("Shrink", "Vähenda"), + ("Stretch", "Venita"), + ("Scrollbar", "Kerimisriba"), + ("ScrollAuto", "Automaatne kerimine"), + ("Good image quality", "Hea pildikvaliteet"), + ("Balanced", "Tasakaalustatud"), + ("Optimize reaction time", "Optimeeri reageerimisaeg"), + ("Custom", "Kohandatud"), + ("Show remote cursor", "Kuva kaugkursorit"), + ("Show quality monitor", "Kuva kvaliteedijälgija"), + ("Disable clipboard", "Keela lõikelaud"), + ("Lock after session end", "Lukusta pärast seansi lõppu"), + ("Insert Ctrl + Alt + Del", "Sisesta Ctrl + Alt + Del"), ("Insert Lock", "Sisesta lukk"), - ("Refresh", ""), - ("ID does not exist", ""), - ("Failed to connect to rendezvous server", ""), - ("Please try later", ""), - ("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", ""), + ("Refresh", "Värskenda"), + ("ID does not exist", "ID-d ei eksisteeri"), + ("Failed to connect to rendezvous server", "Kohtumisserveriga ühendumine ebaõnnestus"), + ("Please try later", "Palun proovi hiljem"), + ("Remote desktop is offline", "Kaugtöölaud on väljas"), + ("Key mismatch", "Võtme sobimatus"), + ("Timeout", "Ajalõpp"), + ("Failed to connect to relay server", "Releeserveriga ühendumine ebaõnnestus"), + ("Failed to connect via rendezvous server", "Kohtumisserveri kaudu ühendumine ebaõnnestus"), + ("Failed to connect via relay server", "Releeserveri kaudu ühendumine ebaõnnestus"), + ("Failed to make direct connection to remote desktop", "Kaugtöölauaga otsese ühenduse loomine ebaõnnestus"), ("Set Password", "Määra parool"), ("OS Password", "Opsüsteemi parool"), ("install_tip", "Kasutajakonto kontrolli (UAC) tõttu ei saa RustDesk mõnel juhul korralikult kaugjuhtimispoolena töötada. Kontrolli vältimiseks palun klõpsa alloleval nupul, et RustDesk oma süsteemi paigaldada."), - ("Click to upgrade", ""), - ("Click to download", ""), - ("Click to update", ""), - ("Configure", ""), + ("Click to upgrade", "Vajuta täiendamiseks"), + ("Click to download", "Vajuta allalaadimiseks"), + ("Click to update", "Vajuta uuendamiseks"), + ("Configure", "Seadista"), ("config_acc", "Töölaua kaugjuhtimiseks tuleb RustDeskile anda \"juurdepääsetavuse\" õigused."), ("config_screen", "Töölaua kaugjuhtimiseks tuleb RustDeskile anda \"ekraanisalvestuse\" õigused."), - ("Installing ...", ""), - ("Install", ""), - ("Installation", ""), + ("Installing ...", "Installimine..."), + ("Install", "Installi"), + ("Installation", "Paigaldus"), ("Installation Path", "Paigaldustee"), - ("Create start menu shortcuts", ""), - ("Create desktop icon", ""), + ("Create start menu shortcuts", "Loo Start-menüü otseteed"), + ("Create desktop icon", "Loo töölauaikoon"), ("agreement_tip", "Paigalduse alustamisel nõustud litsentsilepinguga."), ("Accept and Install", "Nõustu ja paigalda"), - ("End-user license agreement", ""), - ("Generating ...", ""), - ("Your installation is lower version.", ""), - ("not_close_tcp_tip", "Ara sulge seda akent, kuni kasutad tunnelit"), - ("Listening ...", ""), + ("End-user license agreement", "Lõppkasutaja litsentsileping"), + ("Generating ...", "Loomine..."), + ("Your installation is lower version.", "Sinu paigaldus kasutab vanemat versiooni."), + ("not_close_tcp_tip", "Ära sulge seda akent, kuni kasutad tunnelit"), + ("Listening ...", "Kuulamine..."), ("Remote Host", "Kaughost"), ("Remote Port", "Kaugport"), - ("Action", ""), - ("Add", ""), + ("Action", "Tegevus"), + ("Add", "Lisa"), ("Local Port", "Kohalik port"), ("Local Address", "Kohalik aadress"), ("Change Local Port", "Muuda kohalikku porti"), ("setup_server_tip", "Kiirema ühenduse jaoks palun seadista oma server"), - ("Too short, at least 6 characters.", ""), - ("The confirmation is not identical.", ""), - ("Permissions", ""), - ("Accept", ""), - ("Dismiss", ""), - ("Disconnect", ""), - ("Enable file copy and paste", ""), - ("Connected", ""), - ("Direct and encrypted connection", ""), - ("Relayed and encrypted connection", ""), - ("Direct and unencrypted connection", ""), - ("Relayed and unencrypted connection", ""), + ("Too short, at least 6 characters.", "Liiga lühike, peab olema vähemalt 6 tähemärki."), + ("The confirmation is not identical.", "Kinnitus ei ole identne."), + ("Permissions", "Õigused"), + ("Accept", "Nõustu"), + ("Dismiss", "Loobu"), + ("Disconnect", "Ühendu lahti"), + ("Enable file copy and paste", "Luba failide kopeerimine ja kleepimine"), + ("Connected", "Ühendatud"), + ("Direct and encrypted connection", "Otsene ja krüpteeritud ühendus"), + ("Relayed and encrypted connection", "Vahendatud ja krüpteeritud ühendus"), + ("Direct and unencrypted connection", "Otsene ja krüpteerimata ühendus"), + ("Relayed and unencrypted connection", "Vahendatud ja krüpteerimata ühendus"), ("Enter Remote ID", "Sisesta kaug-ID"), - ("Enter your password", ""), - ("Logging in...", ""), - ("Enable RDP session sharing", ""), + ("Enter your password", "Sisesta oma parool"), + ("Logging in...", "Sisselogimine..."), + ("Enable RDP session sharing", "Luba RDP-seansi jagamine"), ("Auto Login", "Logi automaatselt sisse (Kehtib vaid valiku \"lukusta pärast seansi lõppu\" lubamisel)"), - ("Enable direct IP access", ""), - ("Rename", ""), - ("Space", ""), - ("Create desktop shortcut", ""), + ("Enable direct IP access", "Luba otsene IP-juurdepääs"), + ("Rename", "Nimeta ümber"), + ("Space", "Ruum"), + ("Create desktop shortcut", "Loo töölauaotsetee"), ("Change Path", "Muuda failiteed"), ("Create Folder", "Loo kaust"), - ("Please enter the folder name", ""), - ("Fix it", ""), - ("Warning", ""), - ("Login screen using Wayland is not supported", ""), - ("Reboot required", ""), - ("Unsupported display server", ""), - ("x11 expected", ""), - ("Port", ""), - ("Settings", ""), - ("Username", ""), - ("Invalid port", ""), - ("Closed manually by the peer", ""), - ("Enable remote configuration modification", ""), - ("Run without install", ""), - ("Connect via relay", ""), - ("Always connect via relay", ""), + ("Please enter the folder name", "Palun sisesta kausta nimi"), + ("Fix it", "Paranda see"), + ("Warning", "Hoiatus"), + ("Login screen using Wayland is not supported", "Waylandi kasutav sisselogimisekraan ei ole toetatud"), + ("Reboot required", "Taaskäivitus nõutud"), + ("Unsupported display server", "Mittetoetatud kuvaserver"), + ("x11 expected", "Oodatakse x11"), + ("Port", "Port"), + ("Settings", "Seaded"), + ("Username", "Kasutajanimi"), + ("Invalid port", "Sobimatu port"), + ("Closed manually by the peer", "Partneri poolt käsitsi suletud"), + ("Enable remote configuration modification", "Luba kaugtöölaua seadistuse muutmine"), + ("Run without install", "Käivita paigaldamata"), + ("Connect via relay", "Ühenda relee kaudu"), + ("Always connect via relay", "Ühenda alati relee kaudu"), ("whitelist_tip", "Ainult lubamisloendis IP saab mulle ligi"), - ("Login", ""), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), + ("Login", "Logi sisse"), + ("Verify", "Kinnita"), + ("Remember me", "Jäta mind meelde"), + ("Trust this device", "Usalda seda seadet"), + ("Verification code", "Kinnituskood"), ("verification_tip", "Registreeritud e-posti aadressile on saadetud kinnituskood, sisselogimise jätkamiseks sisesta kinnituskood."), - ("Logout", ""), - ("Tags", ""), - ("Search ID", ""), + ("Logout", "Logi välja"), + ("Tags", "Sildid"), + ("Search ID", "Otsi ID-d"), ("whitelist_sep", "Eraldatud koma, semikooloni, tühikute või uue reaga"), - ("Add ID", ""), + ("Add ID", "Lisa ID"), ("Add Tag", "Lisa silt"), - ("Unselect all tags", ""), - ("Network error", ""), - ("Username missed", ""), - ("Password missed", ""), + ("Unselect all tags", "Tühista kõik sildid"), + ("Network error", "Võrgu viga"), + ("Username missed", "Kasutajanimi on puudu"), + ("Password missed", "Parool on puudu"), ("Wrong credentials", "Vale kasutajanimi või parool"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "Kinnituskood on vale või aegunud"), ("Edit Tag", "Muuda silti"), ("Forget Password", "Unusta parool"), - ("Favorites", ""), + ("Favorites", "Lemmikud"), ("Add to Favorites", "Lisa lemmikutesse"), ("Remove from Favorites", "Eemalda lemmikutest"), - ("Empty", ""), - ("Invalid folder name", ""), + ("Empty", "Tühi"), + ("Invalid folder name", "Kehtetu kaustanimi"), ("Socks5 Proxy", "Socks5 proksi"), ("Socks5/Http(s) Proxy", "Socks5/Http(s) proksi"), - ("Discovered", ""), + ("Discovered", "Avastatud"), ("install_daemon_tip", "Süsteemikäivitusel käivitamiseks tuleb paigaldada süsteemiteenus."), - ("Remote ID", ""), - ("Paste", ""), - ("Paste here?", ""), + ("Remote ID", "Kaug-ID"), + ("Paste", "Kleebi"), + ("Paste here?", "Kleebid siia?"), ("Are you sure to close the connection?", "Kas soovid kindlasti ühenduse sulgeda?"), - ("Download new version", ""), - ("Touch mode", ""), - ("Mouse mode", ""), + ("Download new version", "Laadi alla uus versioon"), + ("Touch mode", "Puuterežiim"), + ("Mouse mode", "Hiirerežiim"), ("One-Finger Tap", "Ühe sõrme koputus"), ("Left Mouse", "Vasak hiireklahv"), ("One-Long Tap", "Üks pikk koputus"), @@ -263,23 +263,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Canvas Move", "Lõuendi liigutus"), ("Pinch to Zoom", "Näpistus-suum"), ("Canvas Zoom", "Lõuendi suum"), - ("Reset canvas", ""), - ("No permission of file transfer", ""), - ("Note", ""), - ("Connection", ""), + ("Reset canvas", "Lähtesta lõuend"), + ("No permission of file transfer", "Failiülekande luba puudub"), + ("Note", "Märkus"), + ("Connection", "Ühendus"), ("Share Screen", "Jaga ekraani"), - ("Chat", ""), - ("Total", ""), - ("items", ""), - ("Selected", ""), + ("Chat", "Vestlus"), + ("Total", "Kokku"), + ("items", "üksust"), + ("Selected", "Valitud"), ("Screen Capture", "Ekraanisalvestus"), ("Input Control", "Sisendjuhtimine"), ("Audio Capture", "Helisalvestus"), ("File Connection", "Failiühendus"), ("Screen Connection", "Kuvaühendus"), - ("Do you accept?", ""), + ("Do you accept?", "Kas nõustud?"), ("Open System Setting", "Ava süsteemisätted"), - ("How to get Android input permission?", ""), + ("How to get Android input permission?", "Kuidas saada Androidi sisendi luba?"), ("android_input_permission_tip1", "Selleks, et kaugseade saaks sinu Androidi seadet juhtida hiire või puute abil, pead andma RustDeskile \"juurdepääsetavuse\" loa."), ("android_input_permission_tip2", "Palun mine järgmisele süsteemiseadete lehele, leia ja sisesta [Paigaldatud teenused], lülita teenus [RustDesk Input] sisse."), ("android_new_connection_tip", "Saabunud on uus juhtimistaotlus, mis soovib sinu praegust seadet juhtida."), @@ -288,46 +288,46 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "Kasutatav Androidi versioon ei toeta helisalvestust, palun täienda Android 10 või uuemale versioonile."), ("android_start_service_tip", "Koputa [Alusta teenust] või anna [Ekraanisalvestuse] luba, et ekraanijagamisteenust alustada."), ("android_permission_may_not_change_tip", "Loodud ühenduste õigused ei pruugi muutuda enne taasühendamist koheselt."), - ("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", ""), + ("Account", "Konto"), + ("Overwrite", "Ülekirjutamine"), + ("This file exists, skip or overwrite this file?", "See fail eksisteerib, kas soovid selle vahele jätta või ülekirjutada?"), + ("Quit", "Välju"), + ("Help", "Abi"), + ("Failed", "Ebaõnnestus"), + ("Succeeded", "Õnnestus"), + ("Someone turns on privacy mode, exit", "Keegi lülitab sisse privaatsusrežiimi, välju"), + ("Unsupported", "Mittetoetatud"), + ("Peer denied", "Partner keeldus"), + ("Please install plugins", "Palun paigalda pluginad"), + ("Peer exit", "Partner väljub"), + ("Failed to turn off", "Väljalülitamine ebaõnnestus"), + ("Turned off", "Väljalülitatud"), + ("Language", "Keel"), + ("Keep RustDesk background service", "Säilita RustDeski taustateenus"), ("Ignore Battery Optimizations", "Ignoreeri akuoptimeerimisi"), ("android_open_battery_optimizations_tip", "Kui soovid selle funktsiooni keelata, palun mine järgmisele RustDeski rakenduse seadete lehele, leia ja sisesta [Aku], eemalda linnuke valikult [Piiramata]"), - ("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", ""), - ("Are you sure you want to restart", ""), - ("Restarting remote device", ""), + ("Start on boot", "Käivita seadme käivitamisel"), + ("Start the screen sharing service on boot, requires special permissions", "Käivita ekraanijagamise teenus seadme käivitamisel, nõuab eriõigusi"), + ("Connection not allowed", "Ühendus ei ole lubatud"), + ("Legacy mode", "Pärandrežiim"), + ("Map mode", "Kaardirežiim"), + ("Translate mode", "Tõlkerežiim"), + ("Use permanent password", "Kasuta püsiparooli"), + ("Use both passwords", "Kasuta mõlemat parooli"), + ("Set permanent password", "Seadista püsiparool"), + ("Enable remote restart", "Luba kaugtaaskäivitamine"), + ("Restart remote device", "Taaskäivita kaugseade"), + ("Are you sure you want to restart", "Kas oled kindel, et soovid taaskäivitada"), + ("Restarting remote device", "Kaugseadme taaskäivitamine"), ("remote_restarting_tip", "Kaugseade taaskäivitub, palun sulge see sõnumikast ja ühendu mõne aja pärast uuesti püsiva parooliga."), - ("Copied", ""), - ("Exit Fullscreen", "Lahku täisekraanist"), - ("Fullscreen", ""), + ("Copied", "Kopeeritud"), + ("Exit Fullscreen", "Välju täisekraanist"), + ("Fullscreen", "Täisekraan"), ("Mobile Actions", "Mobiilitegevused"), ("Select Monitor", "Vali kuvar"), ("Control Actions", "Juhtimistegevused"), ("Display Settings", "Kuvasätted"), - ("Ratio", ""), + ("Ratio", "Kuvasuhe"), ("Image Quality", "Pildikvaliteet"), ("Scroll Style", "Kerimisstiil"), ("Show Toolbar", "Kuva tööriistariba"), @@ -336,142 +336,142 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Releeühendus"), ("Secure Connection", "Turvaline ühendus"), ("Insecure Connection", "Ebaturvaline ühendus"), - ("Scale original", ""), - ("Scale adaptive", ""), - ("General", ""), - ("Security", ""), - ("Theme", ""), + ("Scale original", "Originaalskaala"), + ("Scale adaptive", "Kohanduv skaala"), + ("General", "Üldine"), + ("Security", "Turvalisus"), + ("Theme", "Teema"), ("Dark Theme", "Tume teema"), ("Light Theme", "Hele teema"), - ("Dark", ""), - ("Light", ""), + ("Dark", "Tume"), + ("Light", "Hele"), ("Follow System", "Järgi süsteemi"), - ("Enable hardware codec", ""), + ("Enable hardware codec", "Luba riistvarakooder"), ("Unlock Security Settings", "Lukusta lahti turvasätted"), - ("Enable audio", ""), + ("Enable audio", "Luba heli"), ("Unlock Network Settings", "Lukusta lahti võrgusätted"), - ("Server", ""), + ("Server", "Server"), ("Direct IP Access", "Otsene IP-ligipääs"), - ("Proxy", ""), - ("Apply", ""), - ("Disconnect all devices?", ""), - ("Clear", ""), + ("Proxy", "Proksi"), + ("Apply", "Rakenda"), + ("Disconnect all devices?", "Ühendad kõik seadmed lahti?"), + ("Clear", "Tühjenda"), ("Audio Input Device", "Heli sisendseade"), ("Use IP Whitelisting", "Kasuta IP-lubamisloendit"), - ("Network", ""), + ("Network", "Võrk"), ("Pin Toolbar", "Kinnita tööriistariba"), ("Unpin Toolbar", "Eemalda tööriistariba kinnitus"), - ("Recording", ""), - ("Directory", ""), - ("Automatically record incoming sessions", ""), - ("Automatically record outgoing 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...", ""), + ("Recording", "Salvestamine"), + ("Directory", "Kaust"), + ("Automatically record incoming sessions", "Salvesta alati sisenevad ühendused"), + ("Automatically record outgoing sessions", "Salvesta alati väljuvad ühendused"), + ("Change", "Muuda"), + ("Start session recording", "Alusta seansisalvestust"), + ("Stop session recording", "Peata seansisalvestus"), + ("Enable recording session", "Luba seansisalvestus"), + ("Enable LAN discovery", "Luba LAN-avastamine"), + ("Deny LAN discovery", "Keela LAN-avastamine"), + ("Write a message", "Kirjuta sõnum"), + ("Prompt", "Küsi"), + ("Please wait for confirmation of UAC...", "Palun oota UAC (kasutajakonto kontroll) kinnitust..."), ("elevated_foreground_window_tip", "Kaugtöölaua praegune aken nõuab töötamiseks kõrgemaid õigusi, mistõttu ei saa see ajutiselt hiirt ja klaviatuuri kasutada. Võid kaugkasutajal paluda minimeerida praegune aken või klõpsata ühenduse haldamise aknas kõrgendatud loa nuppu. Selle probleemi vältimiseks on soovitatav kaugseadmesse tarkvara paigaldada."), - ("Disconnected", ""), - ("Other", ""), - ("Confirm before closing multiple tabs", ""), + ("Disconnected", "Ühendus katkestatud"), + ("Other", "Muu"), + ("Confirm before closing multiple tabs", "Kinnita enne mitme vahekaardi sulgemist"), ("Keyboard Settings", "Klaviatuurisätted"), ("Full Access", "Täielik ligipääs"), ("Screen Share", "Ekraanijagamine"), - ("Wayland requires Ubuntu 21.04 or higher version.", ""), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), - ("JumpLink", "Kuva"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nõuab Ubuntu 21.04 või uuemat versiooni."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nõuab Linuxi distributsiooni uuemat versiooni. Palun proovi X11 töölaual või muuda oma operatsioonisüsteemi."), + ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Palun vali jagatav ekraan (tegutse partneri poolel)."), - ("Show 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...", ""), + ("Show RustDesk", "Kuva RustDesk"), + ("This PC", "See arvuti"), + ("or", "või"), + ("Continue with", "Jätka koos"), + ("Elevate", "Tõsta"), + ("Zoom cursor", "Suumi kursorit"), + ("Accept sessions via password", "Aktsepteeri seansid parooli kaudu"), + ("Accept sessions via click", "Aktsepteeri seansid klõpsamise kaudu"), + ("Accept sessions via both", "Aktsepteeri seansid mõlema kaudu"), + ("Please wait for the remote side to accept your session request...", "Palun oota, kuni kaugpool aktsepteerib sinu seansitaotluse..."), ("One-time Password", "Ühekordne parool"), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), - ("Hide connection management window", ""), + ("Use one-time password", "Kasuta ühekordset parooli"), + ("One-time password length", "Ühekordse parooli pikkus"), + ("Request access to your device", "Taotle oma seadmele juurdepääsu"), + ("Hide connection management window", "Peida ühenduse haldamise aken"), ("hide_cm_tip", "Luba varjamine ainult siis, kui parooliga seansse võetakse vastu ning kasutatakse püsivat parooli."), ("wayland_experiment_tip", "Waylandi tugi on katsetusjärgus, järelvalveta juurdepääsu vajadusel palun kasuta X11."), - ("Right click to select tabs", ""), - ("Skipped", ""), - ("Add to address book", ""), - ("Group", ""), - ("Search", ""), - ("Closed manually by web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Right click to select tabs", "Paremklõpsa vahekaartide valimiseks"), + ("Skipped", "Vahelejäetud"), + ("Add to address book", "Lisa aadressiraamatusse"), + ("Group", "Grupeeri"), + ("Search", "Otsi"), + ("Closed manually by web console", "Veebikonsooli kaudu käsitsi suletud"), + ("Local keyboard type", "Kohalik klaviatuuritüüp"), + ("Select local keyboard type", "Vali kohalik klaviatuuritüüp"), ("software_render_tip", "Kui kasutad Linuxis Nvidia graafikakaarti ja kaugaken sulgub kohe pärast ühendamist, võib aidata üleminek avatud lähtekoodiga Nouveau draiverile ja valida tarkvaraline renderdamise. Vajalik on tarkvara taaskäivitamine."), - ("Always use software rendering", ""), + ("Always use software rendering", "Kasuta alati tarkvaralist renderdust"), ("config_input", "Kaugtöölaua klaviatuuriga juhtimiseks pead andma RustDeskile \"sisendi jälgimise\" õigused."), ("config_microphone", "Kaugelt rääkimiseks pead andma RustDeskile \"heli salvestamise\" õigused."), ("request_elevation_tip", "Sa võid kõrgendatud õigusi taotleda ka siis, kui keegi on kaugpoolel."), - ("Wait", ""), + ("Wait", "Oota"), ("Elevation Error", "Kõrgendatud õiguste viga"), - ("Ask the remote user for authentication", ""), - ("Choose this if the remote account is administrator", ""), - ("Transmit the username and password of administrator", ""), + ("Ask the remote user for authentication", "Küsi kaugkasutajalt autentimist"), + ("Choose this if the remote account is administrator", "Vali see, kui kaugkonto on administraator"), + ("Transmit the username and password of administrator", "Edasta administraatori kasutajanimi ja parool"), ("still_click_uac_tip", "Kaugkasutaja peab siiski ise vajutama käitatud RustDeski kasutajakonto kontrollis (UAC) OK-nuppu."), ("Request Elevation", "Taotle kõrgendatud õigusi"), ("wait_accept_uac_tip", "Palun oota, kuni kaugkasutaja nõustub UAC-dialoogiga (kasutajakonto kontroll)."), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("Elevate successfully", "Kõrgendamine õnnestus"), + ("uppercase", "suurtäht"), + ("lowercase", "väiketäht"), + ("digit", "number"), + ("special character", "erimärk"), + ("length>=8", "pikkus>=8"), + ("Weak", "Nõrk"), + ("Medium", "Keskmine"), + ("Strong", "Tugev"), ("Switch Sides", "Vaheta pooli"), - ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), + ("Please confirm if you want to share your desktop?", "Palun kinnita, kas soovid oma töölauda jagada?"), + ("Display", "Kuva"), ("Default View Style", "Vaikimisi kuvastiil"), ("Default Scroll Style", "Vaikimisi kerimisstiil"), ("Default Image Quality", "Vaikimisi pildikvaliteet"), ("Default Codec", "Vaikimisi koodek"), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), + ("Bitrate", "Bitikiirus"), + ("FPS", "FPS"), + ("Auto", "Automaatne"), ("Other Default Options", "Teised vaikevalikud"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Häälkõne"), + ("Text chat", "Tekstivestlus"), + ("Stop voice call", "Peata häälkõne"), ("relay_hint_tip", "Otseühendust ei pruugi olla võimalik luua; võid proovida ühendust relee kaudu. Lisaks, kui soovid esimesel katsel kasutada releed, võid lisada ID-le järelliite \"/r\" või valida viimaste seansside kaardil - kui see on olemas - valiku \"Ühenda alati relee kaudu\"."), - ("Reconnect", ""), - ("Codec", ""), - ("Resolution", ""), - ("No transfers in progress", ""), - ("Set one-time password length", ""), + ("Reconnect", "Ühenda uuesti"), + ("Codec", "Koodek"), + ("Resolution", "Resolutsioon"), + ("No transfers in progress", "Ülekandeid ei toimu"), + ("Set one-time password length", "Seadista ühekordse parooli pikkus"), ("RDP Settings", "RDP seaded"), - ("Sort by", ""), + ("Sort by", "Sorteeri"), ("New Connection", "Uus ühendus"), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), + ("Restore", "Taasta"), + ("Minimize", "Minimeeri"), + ("Maximize", "Maksimeeri"), ("Your Device", "Sinu seade"), ("empty_recent_tip", "Ups, hiljutised seansid puuduvad!\nAeg uus planeerida."), ("empty_favorite_tip", "Ei ole veel ühtegi lemmikpartnerit?\nLeia keegi, kellega suhelda ja lisa ta oma lemmikute hulka!"), ("empty_lan_tip", "Oh ei, tundub, et me pole veel ühtegi partnerit avastanud."), ("empty_address_book_tip", "Oh ei, tundub et sinu aadressiraamatus ei ole hetkel ühtegi partnerit."), - ("eg: admin", ""), + ("eg: admin", "nt admin"), ("Empty Username", "Tühi kasutajanimi"), ("Empty Password", "Tühi parool"), - ("Me", ""), + ("Me", "Mina"), ("identical_file_tip", "See fail on partneri omaga identne."), ("show_monitors_tip", "Kuva kuvarid tööriistaribal"), ("View Mode", "Kuvarežiim"), ("login_linux_tip", "X-töölaua seansi lubamiseks pead sisse logima Linuxi kaugkontosse."), - ("verify_rustdesk_password_tip", "Kinnita RustDeski parooli"), + ("verify_rustdesk_password_tip", "Kinnita RustDeski parool"), ("remember_account_tip", "Jäta see konto meelde"), ("os_account_desk_tip", "Seda kontot kasutatakse kaug-opsüsteemi sisselogimiseks ja töölaua seansi lubamiseks headless-režiimis."), ("OS Account", "Opsüsteemi konto"), @@ -481,49 +481,49 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("xorg_not_found_text_tip", "Palun paigalda Xorg"), ("no_desktop_title_tip", "Töölaud pole saadaval"), ("no_desktop_text_tip", "Palun paigalda GNOME Desktop"), - ("No need to elevate", ""), + ("No need to elevate", "Kõrgendamine pole vajalik"), ("System Sound", "Süsteemiheli"), - ("Default", ""), - ("New RDP", ""), - ("Fingerprint", ""), + ("Default", "Vaikimisi"), + ("New RDP", "Uus RDP"), + ("Fingerprint", "Sõrmejälg"), ("Copy Fingerprint", "Kopeeri sõrmejälg"), ("no fingerprints", "Sõrmejäljed puuduvad"), - ("Select a peer", ""), - ("Select peers", ""), - ("Plugins", ""), - ("Uninstall", ""), - ("Update", ""), - ("Enable", ""), - ("Disable", ""), - ("Options", ""), + ("Select a peer", "Vali partner"), + ("Select peers", "Vali partnerid"), + ("Plugins", "Pluginad"), + ("Uninstall", "Desinstalli"), + ("Update", "Uuenda"), + ("Enable", "Luba"), + ("Disable", "Keela"), + ("Options", "Valikud"), ("resolution_original_tip", "Originaalne eraldusvõime"), ("resolution_fit_local_tip", "Ühita kohaliku eraldusvõimega"), ("resolution_custom_tip", "Kohandatud eraldusvõime"), - ("Collapse toolbar", ""), - ("Accept and Elevate", "Luba kõrgemate õigustega"), + ("Collapse toolbar", "Kompaktne tööriistariba"), + ("Accept and Elevate", "Luba kõrgendatud õigustega"), ("accept_and_elevate_btn_tooltip", "Võta ühendus vastu ja anna kõrgemad UAC-õigused (kasutajakonto kontroll)."), ("clipboard_wait_response_timeout_tip", "Koopia vastuse ootamisel tekkis ajalõpp."), - ("Incoming connection", ""), - ("Outgoing connection", ""), - ("Exit", ""), - ("Open", ""), + ("Incoming connection", "Sissetulev ühendus"), + ("Outgoing connection", "Väljuv ühendus"), + ("Exit", "Välju"), + ("Open", "Ava"), ("logout_tip", "Kas soovid kindlasti välja logida?"), - ("Service", ""), - ("Start", ""), - ("Stop", ""), + ("Service", "Teenused"), + ("Start", "Käivita"), + ("Stop", "Peata"), ("exceed_max_devices", "Oled saavutanud hallatavate seadmete maksimaalse arvu."), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), - ("Already exists", ""), + ("Sync with recent sessions", "Sünkroniseeri viimaste seanssidega"), + ("Sort tags", "Sorteeri silte"), + ("Open connection in new tab", "Ava ühendus uuel vahekaardil"), + ("Move tab to new window", "Liiguta vahekaart uude aknasse"), + ("Can not be empty", "Ei tohi olla tühi"), + ("Already exists", "Juba eksisteerib"), ("Change Password", "Vaheta parooli"), ("Refresh Password", "Värskenda parool"), - ("ID", ""), + ("ID", "ID"), ("Grid View", "Ruudustikuvaade"), ("List View", "Loendivaade"), - ("Select", ""), + ("Select", "Vali"), ("Toggle Tags", "Lülita silte"), ("pull_ab_failed_tip", "Aadressiraamatu värskendamine ebaõnnestus"), ("push_ab_failed_tip", "Aadressiraamatu sünkroonimine serveriga ebaõnnestus"), @@ -532,130 +532,130 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Primary Color", "Põhivärv"), ("HSV Color", "HSV-värv"), ("Installation Successful!", "Paigaldus oli edukas!"), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), + ("Installation failed!", "Paigaldus ebaõnnestus!"), + ("Reverse mouse wheel", "Pööra hiireratas"), + ("{} sessions", "{} seanssi"), ("scam_title", "Võid olla KELMUSE ohver!"), ("scam_text1", "Kui räägid telefoniga kellegagi, keda EI TUNNE ja EI USALDA, kes on palunud sul RustDeski kasutada ja teenus käivitada, ära jätka ning lõpeta kõne koheselt."), ("scam_text2", "Tõenäoliselt on tegemist petturiga, kes üritab sinu raha või muid privaatseid andmeid varastada."), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), + ("Don't show again", "Ära kuva uuesti"), + ("I Agree", "Nõustun"), + ("Decline", "Keeldu"), + ("Timeout in minutes", "Ajalõpp minutites"), ("auto_disconnect_option_tip", "Sissetulevate seansside automaatne sulgemine kasutaja mitteaktiivsuse korral"), ("Connection failed due to inactivity", "Mitteaktiivsuse tõttu automaatselt lahti ühendatud"), - ("Check for software update on startup", ""), + ("Check for software update on startup", "Kontrolli käivitusel tarkvarauuendust"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Palun täienda RustDesk Server Pro versioonile {} või uuem!"), ("pull_group_failed_tip", "Grupi värskendamine ebaõnnestus"), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), + ("Filter by intersection", "Filtreeri ristumiste järgi"), + ("Remove wallpaper during incoming sessions", "Eemalda taustapilt sissetulevate seansside ajal"), + ("Test", "Test"), ("display_is_plugged_out_msg", "See kuvar on välja lülitatud, lülita esmasele kuvarile."), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), + ("No displays", "Kuvarid puuduvad"), + ("Open in new window", "Ava uues aknas"), + ("Show displays as individual windows", "Kuva kuvarid eraldi akendena"), + ("Use all my displays for the remote session", "Kasuta kõiki minu kuvarid kaugseansi jaoks"), ("selinux_tip", "SELinux on su seadmes lubatud, mis võib RustDeskil takistada juhitud poolel toimimist."), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), + ("Change view", "Muuda vaadet"), + ("Big tiles", "Suured plaadid"), + ("Small tiles", "Väikesed plaadid"), + ("List", "Loend"), + ("Virtual display", "Virtuaalne kuvar"), + ("Plug out all", "Eemalda kõik"), + ("True color (4:4:4)", "Tõeline värv (4:4:4)"), + ("Enable blocking user input", "Luba kasutaja sisendi blokeerimine"), ("id_input_tip", "Võid sisestada ID, otsese IP või domeeni koos pordiga (:).\nKui soovid juurdepääsu seadmele mõnes teises serveris, lisa palun serveri aadress (@?key=), näiteks,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nKui soovid juurdepääsu seadmele avalikus serveris, sisesta \"@public\", avaliku serveri puhul ei ole võtit vaja."), ("privacy_mode_impl_mag_tip", "Režiim 1"), ("privacy_mode_impl_virtual_display_tip", "Režiim 2"), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), + ("Enter privacy mode", "Sisene privaatsusrežiimi"), + ("Exit privacy mode", "Välju privaatsusrežiimist"), ("idd_not_support_under_win10_2004_tip", "Kaudse kuvari draiver ei ole toetatud. Vajalik on Windows 10, versioon 2004 või uuem."), ("input_source_1_tip", "Sisendallikas 1"), ("input_source_2_tip", "Sisendallikas 2"), - ("Swap control-command key", ""), + ("Swap control-command key", "Vaheta klahvid Control ja Command"), ("swap-left-right-mouse", "Vaheta vasak ja parem hiirenupp"), - ("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", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), - ("Accessible devices", ""), + ("2FA code", "2FA kood"), + ("More", "Rohkem"), + ("enable-2fa-title", "Luba kaheastmeline autentimine"), + ("enable-2fa-desc", "Palun seadista oma autentimisrakendus nüüd. Sa saad kasutada autentimisrakendust nagu Authy, Microsoft või Google Authenticator oma telefonis või lauaarvutis.\n\nSkaneeri QR-kood oma rakendusega ja sisesta kood, mida sinu rakendus näitab, et lubada kaheastmeline autentimine."), + ("wrong-2fa-code", "Koodi ei saa kinnitada. Kontrolli, et kood ja kohalikud ajaseaded oleksid õiged."), + ("enter-2fa-title", "Kaheastmeline autentimine"), + ("Email verification code must be 6 characters.", "E-posti kinnituskood peab olema 6 tähemärki."), + ("2FA code must be 6 digits.", "2FA kood peab olema 6 numbrit."), + ("Multiple Windows sessions found", "Leitud mitu Windowsi seanssi"), + ("Please select the session you want to connect to", "Palun vali seanss, millega soovid ühendada"), + ("powered_by_me", "Põhineb RustDeskil"), + ("outgoing_only_desk_tip", "See on kohandatud versioon.\nSa saad ühenduda teiste seadmetega, kuid teised seadmed ei saa sinu seadmega ühenduda."), + ("preset_password_warning", "See kohandatud versioon sisaldab eelmääratud parooli. Igaüks, kes seda parooli teab, võib saada täieliku kontrolli sinu seadme üle. Kui sa ei oodanud seda, desinstalli tarkvara kohe."), + ("Security Alert", "Turvahoiatus"), + ("My address book", "Minu aadressiraamat"), + ("Personal", "Isiklik"), + ("Owner", "Omanik"), + ("Set shared password", "Seadista jagatud parool"), + ("Exist in", "Eksisteerib"), + ("Read-only", "Ainult lugemiseks"), + ("Read/Write", "Lugemiseks/Kirjutamiseks"), + ("Full Control", "Täielik kontroll"), + ("share_warning_tip", "Ülalolevad väljad on teistele jagatud ja nähtavad."), + ("Everyone", "Igaüks"), + ("ab_web_console_tip", "Rohkem leiad veebikonsoolist"), + ("allow-only-conn-window-open-tip", "Luba ühendus ainult siis, kui RustDeski aken on avatud."), + ("no_need_privacy_mode_no_physical_displays_tip", "Füüsilisi ekraane pole, privaatsusrežiimi kasutamine pole vajalik."), + ("Follow remote cursor", "Jälgi kaugkursorit"), + ("Follow remote window focus", "Jälgi kaugakna fookust"), + ("default_proxy_tip", "Vaikimisi protokoll ja port on Socks5 ja 1080."), + ("no_audio_input_device_tip", "Heli sisendseadet ei leitud."), + ("Incoming", "Sissetulev"), + ("Outgoing", "Väljuv"), + ("Clear Wayland screen selection", "Tühjenda Waylandi ekraanivalik"), + ("clear_Wayland_screen_selection_tip", "Pärast ekraanivaliku tühistamist saad uuesti jagatava ekraani valida."), + ("confirm_clear_Wayland_screen_selection_tip", "Kas oled kindel, et soovid Waylandi ekraanivaliku tühistada?"), + ("android_new_voice_call_tip", "Uus häälkõne taotlus on saadud. Vastu võtmisel lülitub heli häälkommunikatsioonile."), + ("texture_render_tip", "Kasuta tekstuurirenderdust, et muuta pildid sujuvamaks. Renderdusprobleemide esinemisel võid proovida selle valiku keelata."), + ("Use texture rendering", "Kasuta tekstuurirenderdust"), + ("Floating window", "Ujuv aken"), + ("floating_window_tip", "See aitab säilitada RustDeski taustateenust."), + ("Keep screen on", "Hoia ekraan sees"), + ("Never", "Mitte kunagi"), + ("During controlled", "Juhtimise ajal"), + ("During service is on", "Teenuse käitamisel"), + ("Capture screen using DirectX", "Salvesta ekraani DirectX abil"), + ("Back", "Tagasi"), + ("Apps", "Rakendused"), + ("Volume up", "Heli üles"), + ("Volume down", "Heli alla"), + ("Power", "Toide"), + ("Telegram bot", "Telegrami bot"), + ("enable-bot-tip", "Kui lubad selle funktsiooni, saad 2FA koodi oma botilt. See võib töötada ka ühenduse teavitusena."), + ("enable-bot-desc", "1. Ava vestlus kasutajaga @BotFather.\n2. Saada käsklus \"/newbot\". Pärast selle sammu lõpetamist saad tokeni.\n3. Alusta vestlust oma uue loodud botiga. Saada sõnum, mis algab kaldkriipsuga (\"/\") nagu \"/hello\", et see aktiveerida.\n"), + ("cancel-2fa-confirm-tip", "Kas oled kindel, et soovid 2FA tühistada?"), + ("cancel-bot-confirm-tip", "Kas oled kindel, et soovid Telegrami boti tühistada?"), + ("About RustDesk", "RustDeski teave"), + ("Send clipboard keystrokes", "Saada lõikelaua klahvivajutused"), + ("network_error_tip", "Palun kontrolli oma võrguühendust ja seejärel klõpsa nuppu \"Proovi uuesti\"."), + ("Unlock with PIN", "Ava PIN-koodiga"), + ("Requires at least {} characters", "Nõuab vähemalt {} tähemärki"), + ("Wrong PIN", "Vale PIN"), + ("Set PIN", "Seadista PIN"), + ("Enable trusted devices", "Luba usaldusväärsed seadmed"), + ("Manage trusted devices", "Halda usaldusväärseid seadmeid"), + ("Platform", "Platvorm"), + ("Days remaining", "Päevi jäänud"), + ("enable-trusted-devices-tip", "Jäta usaldatud seadmetes 2FA kinnitamine vahele"), + ("Parent directory", "Ülemkaust"), + ("Resume", "Jätka"), + ("Invalid file name", "Kehtetu failinimi"), + ("one-way-file-transfer-tip", "Ühesuunaline failiedastus on lubatud juhitataval poolel."), + ("Authentication Required", "Autentimine nõutud"), + ("Authenticate", "Autendi"), + ("web_id_input_tip", "Saad sisestada sama serveri ID, otse IP-juurdepääs ei ole veebikliendis toetatud.\nKui soovid seadmele teises serveris ligi pääseda, palun lisa serveri aadress (@?key=), näiteks,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nKui soovid seadmele avalikus serveris ligi pääseda, palun sisesta \"@public\"; võti ei ole avaliku serveri jaoks vajalik."), + ("Download", "Laadi alla"), + ("Upload folder", "Laadi kaust üles"), + ("Upload files", "Laadi failid üles"), + ("Clipboard is synchronized", "Lõikelaud on sünkroonitud"), + ("Update client clipboard", "Uuenda kliendi lõikelauda"), + ("Untagged", "Sildistamata"), + ("new-version-of-{}-tip", "Saadaval on {} uus versioon"), + ("Accessible devices", "Ligipääsetavad seadmed"), ].iter().cloned().collect(); } From bdc53f0190a61736412ff494e3eb7d243ff48e85 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 10 Mar 2025 11:09:31 +0900 Subject: [PATCH 136/506] improve lock, https://github.com/rustdesk/rustdesk/issues/11067 --- src/client.rs | 3 ++- src/ui_session_interface.rs | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client.rs b/src/client.rs index d2ceffd3cca..488e388eb6b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -894,7 +894,8 @@ impl ClientClipboardHandler { return; } - if let Some(pi) = ctx.cfg.lc.read().unwrap().peer_info.as_ref() { + let pi = ctx.cfg.lc.read().unwrap().peer_info.clone(); + if let Some(pi) = pi.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, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index cf339f18b82..f2797876a19 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1403,9 +1403,10 @@ impl Session { #[inline] fn try_change_init_resolution(&self, display: i32) { - if let Some((w, h)) = self.lc.read().unwrap().get_custom_resolution(display) { - self.change_resolution(display, w, h); - } + let Some((w, h)) = self.lc.read().unwrap().get_custom_resolution(display) else { + return; + }; + self.change_resolution(display, w, h); } fn do_change_resolution(&self, display: i32, width: i32, height: i32) { From df4a10131636010e0acd1068b786e486db917f46 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 10 Mar 2025 10:16:17 +0800 Subject: [PATCH 137/506] fix: build macos, default feature (#11075) Signed-off-by: fufesou --- libs/clipboard/src/platform/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/clipboard/src/platform/mod.rs b/libs/clipboard/src/platform/mod.rs index f7d28f32267..f54f4021b61 100644 --- a/libs/clipboard/src/platform/mod.rs +++ b/libs/clipboard/src/platform/mod.rs @@ -15,7 +15,7 @@ pub fn create_cliprdr_context( #[cfg(feature = "unix-file-copy-paste")] pub mod unix; -#[cfg(target_os = "macos")] +#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))] pub fn create_cliprdr_context( _enable_files: bool, _enable_others: bool, From f0f999dc279b1d34683549431a7d60020e5f8174 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 10 Mar 2025 21:06:53 +0800 Subject: [PATCH 138/506] view camera (#11040) * view camera Signed-off-by: 21pages * `No cameras` prompt if no cameras available, `peerGetSessionsCount` use connType as parameter Signed-off-by: 21pages * fix, use video_service_name rather than display_idx as key in qos,etc Signed-off-by: 21pages --------- Signed-off-by: 21pages Co-authored-by: Adwin White Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- Cargo.lock | 487 +++++++++--- flutter/lib/common.dart | 78 +- flutter/lib/common/widgets/peer_card.dart | 18 + flutter/lib/common/widgets/remote_input.dart | 49 +- flutter/lib/common/widgets/toolbar.dart | 75 +- flutter/lib/consts.dart | 4 + .../lib/desktop/pages/connection_page.dart | 77 +- .../lib/desktop/pages/desktop_home_page.dart | 14 +- .../desktop/pages/desktop_setting_page.dart | 2 + .../lib/desktop/pages/remote_tab_page.dart | 10 +- flutter/lib/desktop/pages/server_page.dart | 230 +++--- .../lib/desktop/pages/view_camera_page.dart | 730 ++++++++++++++++++ .../desktop/pages/view_camera_tab_page.dart | 499 ++++++++++++ .../screen/desktop_view_camera_screen.dart | 35 + .../lib/desktop/widgets/remote_toolbar.dart | 28 +- .../lib/desktop/widgets/tabbar_widget.dart | 9 +- flutter/lib/main.dart | 27 + flutter/lib/mobile/pages/home_page.dart | 8 +- .../lib/mobile/pages/view_camera_page.dart | 721 +++++++++++++++++ .../lib/models/desktop_render_texture.dart | 11 + flutter/lib/models/input_model.dart | 18 + flutter/lib/models/model.dart | 46 +- flutter/lib/models/server_model.dart | 9 +- flutter/lib/utils/multi_window_manager.dart | 86 ++- flutter/lib/web/bridge.dart | 8 +- flutter/test/cm_test.dart | 8 +- libs/scrap/Cargo.toml | 1 + libs/scrap/src/common/camera.rs | 232 ++++++ libs/scrap/src/common/dxgi.rs | 13 +- libs/scrap/src/common/mod.rs | 1 + libs/scrap/src/common/record.rs | 4 +- libs/scrap/src/common/vram.rs | 27 +- libs/scrap/src/dxgi/mod.rs | 2 +- src/client.rs | 17 +- src/client/io_loop.rs | 55 +- src/core_main.rs | 5 +- src/flutter.rs | 12 +- src/flutter_ffi.rs | 20 +- src/ipc.rs | 3 +- src/lang/ar.rs | 5 + src/lang/be.rs | 5 + src/lang/bg.rs | 5 + src/lang/ca.rs | 5 + src/lang/cn.rs | 5 + src/lang/cs.rs | 5 + src/lang/da.rs | 5 + src/lang/de.rs | 5 + src/lang/el.rs | 5 + src/lang/en.rs | 3 + src/lang/eo.rs | 5 + src/lang/es.rs | 5 + src/lang/et.rs | 7 +- src/lang/eu.rs | 5 + src/lang/fa.rs | 5 + src/lang/fr.rs | 5 + src/lang/he.rs | 5 + src/lang/hr.rs | 5 + src/lang/hu.rs | 5 + src/lang/id.rs | 5 + src/lang/it.rs | 5 + src/lang/ja.rs | 5 + src/lang/ko.rs | 5 + src/lang/kz.rs | 5 + src/lang/lt.rs | 5 + src/lang/lv.rs | 5 + src/lang/nb.rs | 5 + src/lang/nl.rs | 5 + src/lang/pl.rs | 5 + src/lang/pt_PT.rs | 5 + src/lang/ptbr.rs | 5 + src/lang/ro.rs | 5 + src/lang/ru.rs | 5 + src/lang/sc.rs | 5 + src/lang/sk.rs | 5 + src/lang/sl.rs | 5 + src/lang/sq.rs | 5 + src/lang/sr.rs | 5 + src/lang/sv.rs | 5 + src/lang/template.rs | 5 + src/lang/th.rs | 5 + src/lang/tr.rs | 5 + src/lang/tw.rs | 5 + src/lang/uk.rs | 5 + src/lang/vn.rs | 5 + src/server.rs | 60 +- src/server/connection.rs | 209 ++++- src/server/display_service.rs | 1 + src/server/input_service.rs | 9 +- src/server/portable_service.rs | 11 +- src/server/video_qos.rs | 19 +- src/server/video_service.rs | 212 +++-- src/ui/cm.rs | 1 + src/ui/cm.tis | 8 +- src/ui/remote.rs | 3 + src/ui_cm_interface.rs | 9 +- src/ui_session_interface.rs | 11 +- 96 files changed, 3999 insertions(+), 458 deletions(-) create mode 100644 flutter/lib/desktop/pages/view_camera_page.dart create mode 100644 flutter/lib/desktop/pages/view_camera_tab_page.dart create mode 100644 flutter/lib/desktop/screen/desktop_view_camera_screen.dart create mode 100644 flutter/lib/mobile/pages/view_camera_page.dart create mode 100644 libs/scrap/src/common/camera.rs diff --git a/Cargo.lock b/Cargo.lock index 2652296ec1a..621d8d35826 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -242,6 +242,12 @@ dependencies = [ "x11rb 0.13.1", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-broadcast" version = "0.5.1" @@ -384,9 +390,9 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -419,9 +425,9 @@ version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -539,7 +545,7 @@ dependencies = [ "lazycell", "log", "peeking_take_while", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "regex", "rustc-hash", @@ -561,12 +567,12 @@ dependencies = [ "log", "peeking_take_while", "prettyplease", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "regex", "rustc-hash", "shlex", - "syn 2.0.68", + "syn 2.0.98", "which", ] @@ -582,12 +588,12 @@ dependencies = [ "itertools 0.12.1", "lazy_static", "lazycell", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "regex", "rustc-hash", "shlex", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -618,7 +624,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afb15541e888071f64592c0b4364fdff21b7cb0a247f984296699351963a8721" dependencies = [ "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -793,7 +799,7 @@ dependencies = [ "glib 0.18.5", "libc", "once_cell", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -809,13 +815,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.102" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779e6b7d17797c0b42023d417228c02889300190e700cb074c3438d9c541d332" +checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -993,7 +999,7 @@ dependencies = [ "rand 0.8.5", "serde 1.0.203", "serde_derive", - "thiserror", + "thiserror 1.0.61", "utf16string", "uuid", "x11-clipboard 0.8.1", @@ -1045,6 +1051,21 @@ dependencies = [ "cc", ] +[[package]] +name = "cocoa" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c49e86fc36d5704151f5996b7b3795385f50ce09e3be0f47a0cfde869681cf8" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.7.0", + "core-graphics 0.19.2", + "foreign-types 0.3.2", + "libc", + "objc", +] + [[package]] name = "cocoa" version = "0.24.1" @@ -1129,7 +1150,7 @@ source = "git+https://github.com/rustdesk-org/confy#83db9ec19a2f97e9718aef69e4fc dependencies = [ "directories-next", "serde 1.0.203", - "thiserror", + "thiserror 1.0.61", "toml 0.5.11", ] @@ -1164,7 +1185,7 @@ version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "unicode-xid 0.2.4", ] @@ -1181,6 +1202,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -1200,6 +1231,12 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -1214,6 +1251,18 @@ dependencies = [ "objc2-encode 2.0.0-pre.2", ] +[[package]] +name = "core-graphics" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.7.0", + "foreign-types 0.3.2", + "libc", +] + [[package]] name = "core-graphics" version = "0.22.3" @@ -1275,6 +1324,31 @@ dependencies = [ "libc", ] +[[package]] +name = "core-media-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273bf3fc5bf51fd06a7766a84788c1540b6527130a0bce39e00567d6ab9f31f1" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-video-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "libc", + "metal", + "objc", +] + [[package]] name = "coreaudio-rs" version = "0.11.3" @@ -1614,7 +1688,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -1748,7 +1822,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a09ac8bb8c16a282264c379dffba707b9c998afc7506009137f3c6136888078" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -1800,6 +1874,12 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dylib_virtual_display" version = "0.1.0" @@ -1809,7 +1889,7 @@ dependencies = [ "lazy_static", "serde 1.0.203", "serde_derive", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -1859,7 +1939,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06c36cb11dbde389f4096111698d8b567c0720e3452fd5ac3e6b4e47e1939932" dependencies = [ - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -1877,9 +1957,9 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -1898,9 +1978,9 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -2102,7 +2182,7 @@ dependencies = [ "log", "nu-ansi-term", "regex", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -2111,6 +2191,9 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ + "futures-core", + "futures-sink", + "nanorand", "spin", ] @@ -2186,9 +2269,9 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -2353,9 +2436,9 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -2510,8 +2593,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -2546,7 +2631,7 @@ dependencies = [ "once_cell", "pin-project-lite", "smallvec", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -2614,7 +2699,7 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -2628,7 +2713,7 @@ dependencies = [ "itertools 0.9.0", "proc-macro-crate 0.1.5", "proc-macro-error", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -2642,9 +2727,9 @@ dependencies = [ "heck 0.4.1", "proc-macro-crate 2.0.2", "proc-macro-error", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -2716,7 +2801,7 @@ dependencies = [ "once_cell", "paste", "pretty-hex", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -2875,9 +2960,9 @@ checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro-error", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -2967,7 +3052,7 @@ dependencies = [ "socket2 0.3.19", "sodiumoxide", "sysinfo", - "thiserror", + "thiserror 1.0.61", "tokio", "tokio-native-tls", "tokio-rustls 0.26.0", @@ -3252,7 +3337,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", ] @@ -3384,7 +3469,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.61", "walkdir", ] @@ -3399,7 +3484,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.61", "walkdir", "windows-sys 0.45.0", ] @@ -3804,6 +3889,21 @@ dependencies = [ "autocfg 1.3.0", ] +[[package]] +name = "metal" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e198a0ee42bdbe9ef2c09d0b9426f3b2b47d90d93a4a9b0395c4cea605e92dc0" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa 0.20.2", + "core-graphics 0.19.2", + "foreign-types 0.3.2", + "log", + "objc", +] + [[package]] name = "mime" version = "0.3.17" @@ -3838,6 +3938,31 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mozjpeg" +version = "0.10.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55571bce4f12d80ceb4296526e7614f796df72daaaac85f265ab732fa47b7bc9" +dependencies = [ + "arrayvec", + "bytemuck", + "libc", + "mozjpeg-sys", + "rgb", +] + +[[package]] +name = "mozjpeg-sys" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad3626d7942d5b56cc6d47b1c59724c0a976b786fca059c5aaa904aef6324d55" +dependencies = [ + "cc", + "dunce", + "libc", + "nasm-rs", +] + [[package]] name = "muda" version = "0.13.5" @@ -3853,7 +3978,7 @@ dependencies = [ "objc", "once_cell", "png", - "thiserror", + "thiserror 1.0.61", "windows-sys 0.52.0", ] @@ -3863,6 +3988,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "nasm-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12fcfa1bd49e0342ec1d07ed2be83b59963e7acbeb9310e1bb2c07b69dadd959" +dependencies = [ + "jobserver", +] + [[package]] name = "native-tls" version = "0.2.12" @@ -3903,7 +4046,7 @@ dependencies = [ "ndk-sys 0.4.1+23.1.7779620", "num_enum 0.5.11", "raw-window-handle 0.5.2", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -3917,7 +4060,7 @@ dependencies = [ "log", "ndk-sys 0.5.0+25.2.9519653", "num_enum 0.7.2", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -3979,7 +4122,7 @@ dependencies = [ "anyhow", "byteorder", "paste", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -4043,6 +4186,67 @@ dependencies = [ "libc", ] +[[package]] +name = "nokhwa" +version = "0.10.7" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +dependencies = [ + "flume", + "image 0.25.1", + "nokhwa-bindings-linux", + "nokhwa-bindings-macos", + "nokhwa-bindings-windows", + "nokhwa-core", + "paste", + "thiserror 2.0.11", +] + +[[package]] +name = "nokhwa-bindings-linux" +version = "0.1.1" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +dependencies = [ + "nokhwa-core", + "v4l", +] + +[[package]] +name = "nokhwa-bindings-macos" +version = "0.2.2" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +dependencies = [ + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-media-sys", + "core-video-sys", + "flume", + "nokhwa-core", + "objc", + "once_cell", +] + +[[package]] +name = "nokhwa-bindings-windows" +version = "0.4.2" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +dependencies = [ + "nokhwa-core", + "once_cell", + "windows 0.43.0", +] + +[[package]] +name = "nokhwa-core" +version = "0.1.5" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +dependencies = [ + "bytes", + "image 0.25.1", + "mozjpeg", + "thiserror 2.0.11", +] + [[package]] name = "nom" version = "7.1.3" @@ -4102,7 +4306,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -4113,9 +4317,9 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -4191,7 +4395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -4203,9 +4407,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro-crate 2.0.2", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -4440,9 +4644,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -4560,7 +4764,7 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -4745,9 +4949,9 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -4861,8 +5065,8 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ - "proc-macro2 1.0.86", - "syn 2.0.68", + "proc-macro2 1.0.93", + "syn 2.0.98", ] [[package]] @@ -4910,7 +5114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", "version_check", @@ -4922,7 +5126,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "version_check", ] @@ -4938,9 +5142,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -4954,7 +5158,7 @@ dependencies = [ "bytes", "once_cell", "protobuf-support", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -4969,7 +5173,7 @@ dependencies = [ "protobuf-parse", "regex", "tempfile", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -4984,7 +5188,7 @@ dependencies = [ "protobuf", "protobuf-support", "tempfile", - "thiserror", + "thiserror 1.0.61", "which", ] @@ -4994,7 +5198,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e2d30ab1878b2e72d1e2fc23ff5517799c9929e2cf81a8516f9f4dcf2b9cf3" dependencies = [ - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -5078,7 +5282,7 @@ version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", ] [[package]] @@ -5323,7 +5527,7 @@ checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -5412,6 +5616,15 @@ dependencies = [ "winreg 0.50.0", ] +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.8" @@ -5892,6 +6105,7 @@ dependencies = [ "log", "ndk 0.7.0", "ndk-context", + "nokhwa", "num_cpus", "pkg-config", "quest", @@ -5965,9 +6179,9 @@ version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -5999,9 +6213,9 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -6228,7 +6442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" dependencies = [ "heck 0.3.3", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -6240,7 +6454,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "rustversion", "syn 1.0.109", @@ -6269,18 +6483,18 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.68" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "unicode-ident", ] @@ -6345,7 +6559,7 @@ dependencies = [ "pkg-config", "strum 0.18.0", "strum_macros 0.18.0", - "thiserror", + "thiserror 1.0.61", "toml 0.5.11", "version-compare 0.0.10", ] @@ -6418,7 +6632,7 @@ name = "tao-macros" version = "0.1.2" source = "git+https://github.com/rustdesk-org/tao?branch=dev#288c219cb0527e509590c2b2d8e7072aa9feb2d3" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -6513,7 +6727,16 @@ version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.61", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -6522,9 +6745,20 @@ version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", ] [[package]] @@ -6631,9 +6865,9 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -6675,7 +6909,7 @@ checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" dependencies = [ "either", "futures-util", - "thiserror", + "thiserror 1.0.61", "tokio", ] @@ -6690,7 +6924,7 @@ dependencies = [ "futures-sink", "futures-util", "pin-project", - "thiserror", + "thiserror 1.0.61", "tokio", "tokio-util", ] @@ -6819,9 +7053,9 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -6858,7 +7092,7 @@ dependencies = [ "objc2-foundation", "once_cell", "png", - "thiserror", + "thiserror 1.0.61", "windows-sys 0.52.0", ] @@ -7067,6 +7301,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "v4l" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fbfea44a46799d62c55323f3c55d06df722fbe577851d848d328a1041c3403" +dependencies = [ + "bitflags 1.3.2", + "libc", + "v4l2-sys-mit", +] + +[[package]] +name = "v4l2-sys-mit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6779878362b9bacadc7893eac76abe69612e8837ef746573c4a5239daf11990b" +dependencies = [ + "bindgen 0.65.1", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -7129,7 +7383,7 @@ dependencies = [ "dirs 5.0.1", "enquote", "rust-ini", - "thiserror", + "thiserror 1.0.61", "winapi 0.3.9", "winreg 0.11.0", ] @@ -7180,9 +7434,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", "wasm-bindgen-shared", ] @@ -7214,9 +7468,9 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7284,7 +7538,7 @@ version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quick-xml 0.34.0", "quote 1.0.36", ] @@ -7463,6 +7717,21 @@ dependencies = [ "windows_x86_64_msvc 0.34.0", ] +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows" version = "0.44.0" @@ -7547,9 +7816,9 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -7558,9 +7827,9 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -7915,7 +8184,7 @@ dependencies = [ "os_pipe", "rustix 0.38.34", "tempfile", - "thiserror", + "thiserror 1.0.61", "tree_magic_mini", "wayland-backend", "wayland-client", @@ -8094,7 +8363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "regex", "syn 1.0.109", @@ -8136,9 +8405,9 @@ version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -8147,9 +8416,9 @@ version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -8255,7 +8524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", "zvariant_utils", @@ -8267,7 +8536,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 205f30a683a..bf769d4945d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -29,8 +29,10 @@ import '../consts.dart'; import 'common/widgets/overlay.dart'; import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/remote_page.dart'; +import 'mobile/pages/view_camera_page.dart'; import 'desktop/pages/remote_page.dart' as desktop_remote; import 'desktop/pages/file_manager_page.dart' as desktop_file_manager; +import 'desktop/pages/view_camera_page.dart' as desktop_view_camera; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; @@ -96,6 +98,7 @@ enum DesktopType { main, remote, fileTransfer, + viewCamera, cm, portForward, } @@ -1750,7 +1753,8 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async { await bind.setLocalFlutterOption( k: windowFramePrefix + type.name, v: pos.toString()); - if (type == WindowType.RemoteDesktop && windowId != null) { + if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) && + windowId != null) { await _saveSessionWindowPosition( type, windowId, isMaximized, isFullscreen, pos); } @@ -1901,7 +1905,9 @@ Future restoreWindowPosition(WindowType type, String? pos; // No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs) // Though "open in tabs" is true and the new window restore peer position, it's ok. - if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) { + if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) && + windowId != null && + peerId != null) { final peerPos = bind.mainGetPeerFlutterOptionSync( id: peerId, k: windowFramePrefix + type.name); if (peerPos.isNotEmpty) { @@ -1916,7 +1922,7 @@ Future restoreWindowPosition(WindowType type, debugPrint("no window position saved, ignoring position restoration"); return false; } - if (type == WindowType.RemoteDesktop) { + if (type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) { if (!isRemotePeerPos && windowId != null) { if (lpos.offsetWidth != null) { lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset; @@ -2085,6 +2091,7 @@ StreamSubscription? listenUniLinks({handleByFlutter = true}) { enum UriLinkType { remoteDesktop, fileTransfer, + viewCamera, portForward, rdp, } @@ -2136,6 +2143,11 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { id = args[i + 1]; i++; break; + case '--view-camera': + type = UriLinkType.viewCamera; + id = args[i + 1]; + i++; + break; case '--port-forward': type = UriLinkType.portForward; id = args[i + 1]; @@ -2177,6 +2189,12 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { password: password, forceRelay: forceRelay); }); break; + case UriLinkType.viewCamera: + Future.delayed(Duration.zero, () { + rustDeskWinManager.newViewCamera(id!, + password: password, forceRelay: forceRelay); + }); + break; case UriLinkType.portForward: Future.delayed(Duration.zero, () { rustDeskWinManager.newPortForward(id!, false, @@ -2200,7 +2218,14 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { List? urlLinkToCmdArgs(Uri uri) { String? command; String? id; - final options = ["connect", "play", "file-transfer", "port-forward", "rdp"]; + final options = [ + "connect", + "play", + "file-transfer", + "view-camera", + "port-forward", + "rdp" + ]; if (uri.authority.isEmpty && uri.path.split('').every((char) => char == '/')) { return []; @@ -2238,6 +2263,8 @@ List? urlLinkToCmdArgs(Uri uri) { connect(Get.context!, id); } else if (optionIndex == 2) { connect(Get.context!, id, isFileTransfer: true); + } else if (optionIndex == 3) { + connect(Get.context!, id, isViewCamera: true); } return null; } @@ -2290,6 +2317,7 @@ List? urlLinkToCmdArgs(Uri uri) { connectMainDesktop(String id, {required bool isFileTransfer, + required bool isViewCamera, required bool isTcpTunneling, required bool isRDP, bool? forceRelay, @@ -2302,6 +2330,12 @@ connectMainDesktop(String id, isSharedPassword: isSharedPassword, connToken: connToken, forceRelay: forceRelay); + } else if (isViewCamera) { + await rustDeskWinManager.newViewCamera(id, + password: password, + isSharedPassword: isSharedPassword, + connToken: connToken, + forceRelay: forceRelay); } else if (isTcpTunneling || isRDP) { await rustDeskWinManager.newPortForward(id, isRDP, password: password, @@ -2318,10 +2352,12 @@ connectMainDesktop(String id, /// Connect to a peer with [id]. /// If [isFileTransfer], starts a session only for file transfer. +/// If [isViewCamera], starts a session only for view camera. /// If [isTcpTunneling], starts a session only for tcp tunneling. /// If [isRDP], starts a session only for rdp. connect(BuildContext context, String id, {bool isFileTransfer = false, + bool isViewCamera = false, bool isTcpTunneling = false, bool isRDP = false, bool forceRelay = false, @@ -2353,6 +2389,7 @@ connect(BuildContext context, String id, await connectMainDesktop( id, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, isRDP: isRDP, password: password, @@ -2363,6 +2400,7 @@ connect(BuildContext context, String id, await rustDeskWinManager.call(WindowType.Main, kWindowConnect, { 'id': id, 'isFileTransfer': isFileTransfer, + 'isViewCamera': isViewCamera, 'isTcpTunneling': isTcpTunneling, 'isRDP': isRDP, 'password': password, @@ -2400,6 +2438,31 @@ connect(BuildContext context, String id, ), ); } + } else if (isViewCamera) { + if (isWeb) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + desktop_view_camera.ViewCameraPage( + key: ValueKey(id), + id: id, + toolbarState: ToolbarState(), + password: password, + forceRelay: forceRelay, + isSharedPassword: isSharedPassword, + ), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => ViewCameraPage( + id: id, password: password, isSharedPassword: isSharedPassword), + ), + ); + } } else { if (isWeb) { Navigator.push( @@ -2686,6 +2749,8 @@ String getWindowName({WindowType? overrideType}) { return name; case WindowType.FileTransfer: return "File Transfer - $name"; + case WindowType.ViewCamera: + return "View Camera - $name"; case WindowType.PortForward: return "Port Forward - $name"; case WindowType.RemoteDesktop: @@ -3051,6 +3116,7 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi, 'peer_id': peerId, 'display': i, 'display_count': pi.displays.length, + 'window_type': (kWindowType ?? WindowType.RemoteDesktop).index, }; if (screenRect != null) { args['screen_rect'] = { @@ -3065,12 +3131,12 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi, } setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount, - int? display, Rect? screenRect) async { + WindowType windowType, int? display, Rect? screenRect) async { if (screenRect == null) { // Do not restore window position to new connection if there's a pre-session. // https://github.com/rustdesk/rustdesk/discussions/8825 if (preSessionCount == 0) { - await restoreWindowPosition(WindowType.RemoteDesktop, + await restoreWindowPosition(windowType, windowId: windowId, display: display, peerId: peerId); } } else { diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 6d00edc8d72..367aa8685fb 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -488,6 +488,7 @@ abstract class BasePeerCard extends StatelessWidget { BuildContext context, String title, { bool isFileTransfer = false, + bool isViewCamera = false, bool isTcpTunneling = false, bool isRDP = false, }) { @@ -502,6 +503,7 @@ abstract class BasePeerCard extends StatelessWidget { peer, tab, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, isRDP: isRDP, ); @@ -530,6 +532,15 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + MenuEntryBase _viewCameraAction(BuildContext context) { + return _connectCommonAction( + context, + translate('View camera'), + isViewCamera: true, + ); + } + @protected MenuEntryBase _tcpTunnelingAction(BuildContext context) { return _connectCommonAction( @@ -880,6 +891,7 @@ class RecentPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); @@ -939,6 +951,7 @@ class FavoritePeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -992,6 +1005,7 @@ class DiscoveredPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); @@ -1045,6 +1059,7 @@ class AddressBookPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -1177,6 +1192,7 @@ class MyGroupPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -1398,6 +1414,7 @@ class TagPainter extends CustomPainter { void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab, {bool isFileTransfer = false, + bool isViewCamera = false, bool isTcpTunneling = false, bool isRDP = false}) async { var password = ''; @@ -1423,6 +1440,7 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab, password: password, isSharedPassword: isSharedPassword, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, isRDP: isRDP); } diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 6667bdf8026..36fb4533995 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -53,13 +54,14 @@ class RawKeyFocusScope extends StatelessWidget { class RawTouchGestureDetectorRegion extends StatefulWidget { final Widget child; final FFI ffi; - + final bool isCamera; late final InputModel inputModel = ffi.inputModel; late final FfiModel ffiModel = ffi.ffiModel; RawTouchGestureDetectorRegion({ required this.child, required this.ffi, + this.isCamera = false, }); @override @@ -382,6 +384,7 @@ class _RawTouchGestureDetectorRegionState _scale = d.scale; if (scale != 0) { + if (widget.isCamera) return; await bind.sessionSendPointer( sessionId: sessionId, msg: json.encode( @@ -402,6 +405,7 @@ class _RawTouchGestureDetectorRegionState return; } if ((isDesktop || isWebDesktop)) { + if (widget.isCamera) return; await bind.sessionSendPointer( sessionId: sessionId, msg: json.encode( @@ -536,3 +540,46 @@ class RawPointerMouseRegion extends StatelessWidget { ); } } + +class CameraRawPointerMouseRegion extends StatelessWidget { + final InputModel inputModel; + final Widget child; + final PointerEnterEventListener? onEnter; + final PointerExitEventListener? onExit; + final PointerDownEventListener? onPointerDown; + final PointerUpEventListener? onPointerUp; + + CameraRawPointerMouseRegion({ + this.onEnter, + this.onExit, + this.onPointerDown, + this.onPointerUp, + required this.inputModel, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Listener( + onPointerHover: (evt) { + final offset = evt.position; + double x = offset.dx; + double y = max(0.0, offset.dy); + inputModel.handlePointerDevicePos( + kPointerEventKindMouse, x, y, true, kMouseEventTypeDefault); + }, + onPointerDown: (evt) { + onPointerDown?.call(evt); + }, + onPointerUp: (evt) { + onPointerUp?.call(evt); + }, + child: MouseRegion( + cursor: MouseCursor.defer, + onEnter: onEnter, + onExit: onExit, + child: child, + ), + ); + } +} diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 153121057e5..4011110ddcb 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -89,10 +89,13 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { final pi = ffiModel.pi; final perms = ffiModel.permissions; final sessionId = ffi.sessionId; + final isDefaultConn = ffi.connType == ConnType.defaultConn; List v = []; // elevation - if (perms['keyboard'] != false && ffi.elevationModel.showRequestMenu) { + if (isDefaultConn && + perms['keyboard'] != false && + ffi.elevationModel.showRequestMenu) { v.add( TTextMenu( child: Text(translate('Request Elevation')), @@ -101,7 +104,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // osAccount / osPassword - if (perms['keyboard'] != false) { + if (isDefaultConn && perms['keyboard'] != false) { v.add( TTextMenu( child: Row(children: [ @@ -130,7 +133,9 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // paste - if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) { + if (isDefaultConn && + pi.platform != kPeerPlatformAndroid && + perms['keyboard'] != false) { v.add(TTextMenu( child: Text(translate('Send clipboard keystrokes')), onPressed: () async { @@ -142,43 +147,53 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { })); } // reset canvas - if (isMobile) { + if (isDefaultConn && isMobile) { v.add(TTextMenu( child: Text(translate('Reset canvas')), onPressed: () => ffi.cursorModel.reset())); } connectWithToken( - {required bool isFileTransfer, required bool isTcpTunneling}) { + {bool isFileTransfer = false, + bool isViewCamera = false, + bool isTcpTunneling = false}) { final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId); connect(context, id, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, connToken: connToken); } // transferFile - if (isDesktop) { + if (isDefaultConn && isDesktop) { v.add( TTextMenu( child: Text(translate('Transfer file')), - onPressed: () => - connectWithToken(isFileTransfer: true, isTcpTunneling: false)), + onPressed: () => connectWithToken(isFileTransfer: true)), + ); + } + // viewCamera + if (isDefaultConn && isDesktop) { + v.add( + TTextMenu( + child: Text(translate('View camera')), + onPressed: () => connectWithToken(isViewCamera: true)), ); } // tcpTunneling - if (isDesktop) { + if (isDefaultConn && isDesktop) { v.add( TTextMenu( child: Text(translate('TCP tunneling')), - onPressed: () => - connectWithToken(isFileTransfer: false, isTcpTunneling: true)), + onPressed: () => connectWithToken(isTcpTunneling: true)), ); } // note - if (bind - .sessionGetAuditServerSync(sessionId: sessionId, typ: "conn") - .isNotEmpty) { + if (isDefaultConn && + bind + .sessionGetAuditServerSync(sessionId: sessionId, typ: "conn") + .isNotEmpty) { v.add( TTextMenu( child: Text(translate('Note')), @@ -186,11 +201,12 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // divider - if (isDesktop || isWebDesktop) { + if (isDefaultConn && (isDesktop || isWebDesktop)) { v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true)); } // ctrlAltDel - if (!ffiModel.viewOnly && + if (isDefaultConn && + !ffiModel.viewOnly && ffiModel.keyboard && (pi.platform == kPeerPlatformLinux || pi.sasEnabled)) { v.add( @@ -200,7 +216,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // restart - if (perms['restart'] != false && + if (isDefaultConn && + perms['restart'] != false && (pi.platform == kPeerPlatformLinux || pi.platform == kPeerPlatformWindows || pi.platform == kPeerPlatformMacOS)) { @@ -212,7 +229,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // insertLock - if (!ffiModel.viewOnly && ffi.ffiModel.keyboard) { + if (isDefaultConn && !ffiModel.viewOnly && ffi.ffiModel.keyboard) { v.add( TTextMenu( child: Text(translate('Insert Lock')), @@ -220,7 +237,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // blockUserInput - if (ffi.ffiModel.keyboard && + if (isDefaultConn && + ffi.ffiModel.keyboard && ffi.ffiModel.permissions['block_input'] != false && pi.platform == kPeerPlatformWindows) // privacy-mode != true ?? { @@ -236,12 +254,13 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { })); } // switchSides - if (isDesktop && + if (isDefaultConn && + isDesktop && ffiModel.keyboard && pi.platform != kPeerPlatformAndroid && pi.platform != kPeerPlatformMacOS && versionCmp(pi.version, '1.2.0') >= 0 && - bind.peerGetDefaultSessionsCount(id: id) == 1) { + bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) { v.add(TTextMenu( child: Text(translate('Switch Sides')), onPressed: () => @@ -523,6 +542,7 @@ Future> toolbarDisplayToggle( final pi = ffiModel.pi; final perms = ffiModel.permissions; final sessionId = ffi.sessionId; + final isDefaultConn = ffi.connType == ConnType.defaultConn; // show quality monitor final option = 'show-quality-monitor'; @@ -535,7 +555,7 @@ Future> toolbarDisplayToggle( }, child: Text(translate('Show quality monitor')))); // mute - if (perms['audio'] != false) { + if (isDefaultConn && perms['audio'] != false) { final option = 'disable-audio'; final value = bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); @@ -556,7 +576,8 @@ Future> toolbarDisplayToggle( final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 && bind.mainHasFileClipboard() && pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard); - if (ffiModel.keyboard && + if (isDefaultConn && + ffiModel.keyboard && perms['file'] != false && (isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) { final enabled = !ffiModel.viewOnly; @@ -574,7 +595,7 @@ Future> toolbarDisplayToggle( child: Text(translate('Enable file copy and paste')))); } // disable clipboard - if (ffiModel.keyboard && perms['clipboard'] != false) { + if (isDefaultConn && ffiModel.keyboard && perms['clipboard'] != false) { final enabled = !ffiModel.viewOnly; final option = 'disable-clipboard'; var value = @@ -591,7 +612,7 @@ Future> toolbarDisplayToggle( child: Text(translate('Disable clipboard')))); } // lock after session end - if (ffiModel.keyboard && !ffiModel.isPeerAndroid) { + if (isDefaultConn && ffiModel.keyboard && !ffiModel.isPeerAndroid) { final enabled = !ffiModel.viewOnly; final option = 'lock-after-session-end'; final value = @@ -656,12 +677,12 @@ Future> toolbarDisplayToggle( child: Text(translate('True color (4:4:4)')))); } - if (isMobile) { + if (isDefaultConn && isMobile) { v.addAll(toolbarKeyboardToggles(ffi)); } // view mode (mobile only, desktop is in keyboard menu) - if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) { + if (isDefaultConn && isMobile && versionCmp(pi.version, '1.2.0') >= 0) { v.add(TToggleMenu( value: ffiModel.viewOnly, onChanged: (value) async { diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 9b8e45aa4ad..d5b133bf198 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -27,6 +27,7 @@ const String kPlatformAdditionsAmyuniVirtualDisplays = const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard"; const String kPlatformAdditionsSupportedPrivacyModeImpl = "supported_privacy_mode_impl"; +const String kPlatformAdditionsSupportViewCamera = "support_view_camera"; const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformLinux = "Linux"; @@ -44,6 +45,7 @@ const String kAppTypeConnectionManager = "cm"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; +const String kAppTypeDesktopViewCamera = "view camera"; const String kAppTypeDesktopPortForward = "port forward"; const String kWindowMainWindowOnTop = "main_window_on_top"; @@ -58,6 +60,7 @@ const String kWindowConnect = "connect"; const String kWindowEventNewRemoteDesktop = "new_remote_desktop"; const String kWindowEventNewFileTransfer = "new_file_transfer"; +const String kWindowEventNewViewCamera = "new_view_camera"; const String kWindowEventNewPortForward = "new_port_forward"; const String kWindowEventActiveSession = "active_session"; const String kWindowEventActiveDisplaySession = "active_display_session"; @@ -97,6 +100,7 @@ const String kOptionEnableKeyboard = "enable-keyboard"; const String kOptionEnableClipboard = "enable-clipboard"; const String kOptionEnableFileTransfer = "enable-file-transfer"; const String kOptionEnableAudio = "enable-audio"; +const String kOptionEnableCamera = "enable-camera"; const String kOptionEnableTunnel = "enable-tunnel"; const String kOptionEnableRemoteRestart = "enable-remote-restart"; const String kOptionEnableBlockInput = "enable-block-input"; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index dd9e840411f..8efc355f31f 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -17,7 +17,6 @@ import '../../common/formatter/id_formatter.dart'; import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/autocomplete.dart'; import '../../models/platform_model.dart'; -import '../widgets/button.dart'; class OnlineStatusWidget extends StatefulWidget { const OnlineStatusWidget({Key? key, this.onSvcStatusChanged}) @@ -203,6 +202,8 @@ class _ConnectionPageState extends State final FocusNode _idFocusNode = FocusNode(); final TextEditingController _idEditingController = TextEditingController(); + String selectedConnectionType = 'Connect'; + bool isWindowMinimized = false; final AllPeersLoader _allPeersLoader = AllPeersLoader(); @@ -321,9 +322,10 @@ class _ConnectionPageState extends State /// Callback for the connect button. /// Connects to the selected peer. - void onConnect({bool isFileTransfer = false}) { + void onConnect({bool isFileTransfer = false, bool isViewCamera = false}) { var id = _idController.id; - connect(context, id, isFileTransfer: isFileTransfer); + connect(context, id, + isFileTransfer: isFileTransfer, isViewCamera: isViewCamera); } /// UI for the remote ID TextField. @@ -501,21 +503,64 @@ class _ConnectionPageState extends State ), Padding( padding: const EdgeInsets.only(top: 13.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Button( - isOutline: true, - onTap: () => onConnect(isFileTransfer: true), - text: "Transfer file", + child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + SizedBox( + height: 28.0, + child: ElevatedButton( + onPressed: () { + onConnect(); + }, + child: Text(translate("Connect")), ), - const SizedBox( - width: 17, + ), + const SizedBox(width: 3), + Container( + height: 28.0, + width: 28.0, + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(8), ), - Button(onTap: onConnect, text: "Connect"), - ], - ), - ) + child: Center( + child: MenuAnchor( + builder: (context, controller, builder) { + return IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + visualDensity: VisualDensity.compact, + icon: controller.isOpen + ? const Icon(Icons.keyboard_arrow_up) + : const Icon(Icons.keyboard_arrow_down), + onPressed: () { + setState(() { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }); + }, + ); + }, + menuChildren: [ + MenuItemButton( + onPressed: () { + onConnect(isFileTransfer: true); + }, + child: Text(translate('Transfer file')), + ), + MenuItemButton( + onPressed: () { + onConnect(isViewCamera: true); + }, + child: Text(translate('View camera')), + ), + ], + ), + ), + ), + ]), + ), ], ), ), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 94e32575861..7f30a5a63d5 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -775,6 +775,7 @@ class _DesktopHomePageState extends State await connectMainDesktop( call.arguments['id'], isFileTransfer: call.arguments['isFileTransfer'], + isViewCamera: call.arguments['isViewCamera'], isTcpTunneling: call.arguments['isTcpTunneling'], isRDP: call.arguments['isRDP'], password: call.arguments['password'], @@ -789,9 +790,15 @@ class _DesktopHomePageState extends State } catch (e) { debugPrint("Failed to parse window id '${call.arguments}': $e"); } - if (windowId != null) { + WindowType? windowType; + try { + windowType = WindowType.values.byName(args[3]); + } catch (e) { + debugPrint("Failed to parse window type '${call.arguments}': $e"); + } + if (windowId != null && windowType != null) { await rustDeskWinManager.moveTabToNewWindow( - windowId, args[1], args[2]); + windowId, args[1], args[2], windowType); } } else if (call.method == kWindowEventOpenMonitorSession) { final args = jsonDecode(call.arguments); @@ -799,9 +806,10 @@ class _DesktopHomePageState extends State final peerId = args['peer_id'] as String; final display = args['display'] as int; final displayCount = args['display_count'] as int; + final windowType = args['window_type'] as int; final screenRect = parseParamScreenRect(args); await rustDeskWinManager.openMonitorSession( - windowId, peerId, display, displayCount, screenRect); + windowId, peerId, display, displayCount, screenRect, windowType); } else if (call.method == kWindowEventRemoteWindowCoords) { final windowId = int.tryParse(call.arguments); if (windowId != null) { diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index f89381a3ff5..ad9a1296ca6 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -960,6 +960,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { enabled: enabled, fakeValue: fakeValue), _OptionCheckBox(context, 'Enable audio', kOptionEnableAudio, enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable camera', kOptionEnableCamera, + enabled: enabled, fakeValue: fakeValue), _OptionCheckBox( context, 'Enable TCP tunneling', kOptionEnableTunnel, enabled: enabled, fakeValue: fakeValue), diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 025e530c2ce..644f6c3367a 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -269,8 +269,10 @@ class _ConnectionTabPageState extends State { style: style, ), proc: () async { - await DesktopMultiWindow.invokeMethod(kMainWindowId, - kWindowEventMoveTabToNewWindow, '${windowId()},$key,$sessionId'); + await DesktopMultiWindow.invokeMethod( + kMainWindowId, + kWindowEventMoveTabToNewWindow, + '${windowId()},$key,$sessionId,RemoteDesktop'); cancelFunc(); }, padding: padding, @@ -417,8 +419,8 @@ class _ConnectionTabPageState extends State { await WindowController.fromWindowId(windowId()).setFullscreen(false); stateGlobal.setFullscreen(false, procWnd: false); } - await setNewConnectWindowFrame( - windowId(), id!, prePeerCount, display, screenRect); + await setNewConnectWindowFrame(windowId(), id!, prePeerCount, + WindowType.RemoteDesktop, display, screenRect); Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async { await windowOnTop(windowId()); }); diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 9a93cb34f1d..ec081d57428 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -353,7 +353,9 @@ Widget buildConnectionCard(Client client) { key: ValueKey(client.id), children: [ _CmHeader(client: client), - client.type_() != ClientType.remote || client.disconnected + client.type_() == ClientType.file || + client.type_() == ClientType.portForward || + client.disconnected ? Offstage() : _PrivilegeBoard(client: client), Expanded( @@ -526,7 +528,8 @@ class _CmHeaderState extends State<_CmHeader> Offstage( offstage: !client.authorized || (client.type_() != ClientType.remote && - client.type_() != ClientType.file), + client.type_() != ClientType.file && + client.type_() != ClientType.camera), child: IconButton( onPressed: () => checkClickTime(client.id, () { if (client.type_() == ClientType.file) { @@ -627,96 +630,139 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { padding: EdgeInsets.symmetric(horizontal: spacing), mainAxisSpacing: spacing, crossAxisSpacing: spacing, - children: [ - buildPermissionIcon( - client.keyboard, - Icons.keyboard, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "keyboard", enabled: enabled); - setState(() { - client.keyboard = enabled; - }); - }, - translate('Enable keyboard/mouse'), - ), - buildPermissionIcon( - client.clipboard, - Icons.assignment_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "clipboard", enabled: enabled); - setState(() { - client.clipboard = enabled; - }); - }, - translate('Enable clipboard'), - ), - buildPermissionIcon( - client.audio, - Icons.volume_up_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "audio", enabled: enabled); - setState(() { - client.audio = enabled; - }); - }, - translate('Enable audio'), - ), - buildPermissionIcon( - client.file, - Icons.upload_file_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "file", enabled: enabled); - setState(() { - client.file = enabled; - }); - }, - translate('Enable file copy and paste'), - ), - buildPermissionIcon( - client.restart, - Icons.restart_alt_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "restart", enabled: enabled); - setState(() { - client.restart = enabled; - }); - }, - translate('Enable remote restart'), - ), - buildPermissionIcon( - client.recording, - Icons.videocam_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "recording", enabled: enabled); - setState(() { - client.recording = enabled; - }); - }, - translate('Enable recording session'), - ), - // only windows support block input - if (isWindows) - buildPermissionIcon( - client.blockInput, - Icons.block, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, - name: "block_input", - enabled: enabled); - setState(() { - client.blockInput = enabled; - }); - }, - translate('Enable blocking user input'), - ) - ], + children: client.type_() == ClientType.camera + ? [ + buildPermissionIcon( + client.audio, + Icons.volume_up_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "audio", + enabled: enabled); + setState(() { + client.audio = enabled; + }); + }, + translate('Enable audio'), + ), + buildPermissionIcon( + client.recording, + Icons.videocam_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "recording", + enabled: enabled); + setState(() { + client.recording = enabled; + }); + }, + translate('Enable recording session'), + ), + ] + : [ + buildPermissionIcon( + client.keyboard, + Icons.keyboard, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "keyboard", + enabled: enabled); + setState(() { + client.keyboard = enabled; + }); + }, + translate('Enable keyboard/mouse'), + ), + buildPermissionIcon( + client.clipboard, + Icons.assignment_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "clipboard", + enabled: enabled); + setState(() { + client.clipboard = enabled; + }); + }, + translate('Enable clipboard'), + ), + buildPermissionIcon( + client.audio, + Icons.volume_up_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "audio", + enabled: enabled); + setState(() { + client.audio = enabled; + }); + }, + translate('Enable audio'), + ), + buildPermissionIcon( + client.file, + Icons.upload_file_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "file", + enabled: enabled); + setState(() { + client.file = enabled; + }); + }, + translate('Enable file copy and paste'), + ), + buildPermissionIcon( + client.restart, + Icons.restart_alt_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "restart", + enabled: enabled); + setState(() { + client.restart = enabled; + }); + }, + translate('Enable remote restart'), + ), + buildPermissionIcon( + client.recording, + Icons.videocam_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "recording", + enabled: enabled); + setState(() { + client.recording = enabled; + }); + }, + translate('Enable recording session'), + ), + // only windows support block input + if (isWindows) + buildPermissionIcon( + client.blockInput, + Icons.block, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "block_input", + enabled: enabled); + setState(() { + client.blockInput = enabled; + }); + }, + translate('Enable blocking user input'), + ) + ], ), ), ], diff --git a/flutter/lib/desktop/pages/view_camera_page.dart b/flutter/lib/desktop/pages/view_camera_page.dart new file mode 100644 index 00000000000..b06dc86c563 --- /dev/null +++ b/flutter/lib/desktop/pages/view_camera_page.dart @@ -0,0 +1,730 @@ +import 'dart:async'; + +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/remote_input.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:flutter_hbb/models/state_model.dart'; + +import '../../consts.dart'; +import '../../common/widgets/overlay.dart'; +import '../../common.dart'; +import '../../common/widgets/dialog.dart'; +import '../../common/widgets/toolbar.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import '../../common/shared_state.dart'; +import '../../utils/image.dart'; +import '../widgets/remote_toolbar.dart'; +import '../widgets/kb_layout_type_chooser.dart'; +import '../widgets/tabbar_widget.dart'; + +import 'package:flutter_hbb/native/custom_cursor.dart' + if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart'; + +final SimpleWrapper _firstEnterImage = SimpleWrapper(false); + +// Used to skip session close if "move to new window" is clicked. +final Map closeSessionOnDispose = {}; + +class ViewCameraPage extends StatefulWidget { + ViewCameraPage({ + Key? key, + required this.id, + required this.toolbarState, + this.sessionId, + this.tabWindowId, + this.password, + this.display, + this.displays, + this.tabController, + this.connToken, + this.forceRelay, + this.isSharedPassword, + }) : super(key: key) { + initSharedStates(id); + } + + final String id; + final SessionID? sessionId; + final int? tabWindowId; + final int? display; + final List? displays; + final String? password; + final ToolbarState toolbarState; + final bool? forceRelay; + final bool? isSharedPassword; + final String? connToken; + final SimpleWrapper?> _lastState = SimpleWrapper(null); + final DesktopTabController? tabController; + + FFI get ffi => (_lastState.value! as _ViewCameraPageState)._ffi; + + @override + State createState() { + final state = _ViewCameraPageState(id); + _lastState.value = state; + return state; + } +} + +class _ViewCameraPageState extends State + with AutomaticKeepAliveClientMixin, MultiWindowListener { + Timer? _timer; + String keyboardMode = "legacy"; + bool _isWindowBlur = false; + final _cursorOverImage = false.obs; + + var _blockableOverlayState = BlockableOverlayState(); + + final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); + + // We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar` + // to identify the toolbar instance and its callback function. + int? _instanceIdOnEnterOrLeaveImage4Toolbar; + Function(bool)? _onEnterOrLeaveImage4Toolbar; + + late FFI _ffi; + + SessionID get sessionId => _ffi.sessionId; + + _ViewCameraPageState(String id) { + _initStates(id); + } + + void _initStates(String id) {} + + @override + void initState() { + super.initState(); + _ffi = FFI(widget.sessionId); + Get.put(_ffi, tag: widget.id); + _ffi.imageModel.addCallbackOnFirstImage((String peerId) { + showKBLayoutTypeChooserIfNeeded( + _ffi.ffiModel.pi.platform, _ffi.dialogManager); + _ffi.recordingModel + .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId)); + }); + _ffi.start( + widget.id, + isViewCamera: true, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, + tabWindowId: widget.tabWindowId, + display: widget.display, + displays: widget.displays, + connToken: widget.connToken, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + if (!isLinux) { + WakelockPlus.enable(); + } + + _ffi.ffiModel.updateEventListener(sessionId, widget.id); + if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); + _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId); + _ffi.dialogManager.loadMobileActionsOverlayVisible(); + DesktopMultiWindow.addListener(this); + // if (!_isCustomCursorInited) { + // customCursorController.registerNeedUpdateCursorCallback( + // (String? lastKey, String? currentKey) async { + // if (_firstEnterImage.value) { + // _firstEnterImage.value = false; + // return true; + // } + // return lastKey == null || lastKey != currentKey; + // }); + // _isCustomCursorInited = true; + // } + + _blockableOverlayState.applyFfi(_ffi); + // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState. + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.tabController?.onSelected?.call(widget.id); + }); + } + + @override + void onWindowBlur() { + super.onWindowBlur(); + // On windows, we use `focus` way to handle keyboard better. + // Now on Linux, there's some rdev issues which will break the input. + // We disable the `focus` way for non-Windows temporarily. + if (isWindows) { + _isWindowBlur = true; + // unfocus the primary-focus when the whole window is lost focus, + // and let OS to handle events instead. + _rawKeyFocusNode.unfocus(); + } + stateGlobal.isFocused.value = false; + } + + @override + void onWindowFocus() { + super.onWindowFocus(); + // See [onWindowBlur]. + if (isWindows) { + _isWindowBlur = false; + } + stateGlobal.isFocused.value = true; + } + + @override + void onWindowRestore() { + super.onWindowRestore(); + // On windows, we use `onWindowRestore` way to handle window restore from + // a minimized state. + if (isWindows) { + _isWindowBlur = false; + } + if (!isLinux) { + WakelockPlus.enable(); + } + } + + // When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not. + @override + void onWindowMaximize() { + super.onWindowMaximize(); + if (!isLinux) { + WakelockPlus.enable(); + } + } + + @override + void onWindowMinimize() { + super.onWindowMinimize(); + if (!isLinux) { + WakelockPlus.disable(); + } + } + + @override + void onWindowEnterFullScreen() { + super.onWindowEnterFullScreen(); + if (isMacOS) { + stateGlobal.setFullscreen(true); + } + } + + @override + void onWindowLeaveFullScreen() { + super.onWindowLeaveFullScreen(); + if (isMacOS) { + stateGlobal.setFullscreen(false); + } + } + + @override + Future dispose() async { + final closeSession = closeSessionOnDispose.remove(widget.id) ?? true; + + // https://github.com/flutter/flutter/issues/64935 + super.dispose(); + debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); + _ffi.textureModel.onViewCameraPageDispose(closeSession); + 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(); + _ffi.cursorModel.disposeImages(); + _rawKeyFocusNode.dispose(); + await _ffi.close(closeSession: closeSession); + _timer?.cancel(); + _ffi.dialogManager.dismissAll(); + if (closeSession) { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + } + if (!isLinux) { + await WakelockPlus.disable(); + } + await Get.delete(tag: widget.id); + removeSharedStates(widget.id); + } + + Widget emptyOverlay() => BlockableOverlay( + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] + state: _blockableOverlayState, + underlying: Container( + color: Colors.transparent, + ), + ); + + Widget buildBody(BuildContext context) { + remoteToolbar(BuildContext context) => RemoteToolbar( + id: widget.id, + ffi: _ffi, + state: widget.toolbarState, + onEnterOrLeaveImageSetter: (id, func) { + _instanceIdOnEnterOrLeaveImage4Toolbar = id; + _onEnterOrLeaveImage4Toolbar = func; + }, + onEnterOrLeaveImageCleaner: (id) { + // If _instanceIdOnEnterOrLeaveImage4Toolbar != id + // it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar. + if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) { + _instanceIdOnEnterOrLeaveImage4Toolbar = null; + _onEnterOrLeaveImage4Toolbar = null; + } + }, + setRemoteState: setState, + ); + + bodyWidget() { + return Stack( + children: [ + Container( + color: kColorCanvas, + child: getBodyForDesktop(context), + ), + Stack( + children: [ + _ffi.ffiModel.pi.isSet.isTrue && + _ffi.ffiModel.waitForFirstImage.isTrue + ? emptyOverlay() + : () { + if (!_ffi.ffiModel.isPeerAndroid) { + return Offstage(); + } else { + return Obx(() => Offstage( + offstage: _ffi.dialogManager + .mobileActionsOverlayVisible.isFalse, + child: Overlay(initialEntries: [ + makeMobileActionsOverlayEntry( + () => _ffi.dialogManager + .setMobileActionsOverlayVisible(false), + ffi: _ffi, + ) + ]), + )); + } + }(), + // Use Overlay to enable rebuild every time on menu button click. + _ffi.ffiModel.pi.isSet.isTrue + ? Overlay( + initialEntries: [OverlayEntry(builder: remoteToolbar)]) + : remoteToolbar(context), + _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(), + ], + ), + ], + ); + } + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: Obx(() { + final imageReady = _ffi.ffiModel.pi.isSet.isTrue && + _ffi.ffiModel.waitForFirstImage.isFalse; + if (imageReady) { + // If the privacy mode(disable physical displays) is switched, + // we should not dismiss the dialog immediately. + if (DateTime.now().difference(togglePrivacyModeTime) > + const Duration(milliseconds: 3000)) { + // `dismissAll()` is to ensure that the state is clean. + // It's ok to call dismissAll() here. + _ffi.dialogManager.dismissAll(); + // Recreate the block state to refresh the state. + _blockableOverlayState = BlockableOverlayState(); + _blockableOverlayState.applyFfi(_ffi); + } + // Block the whole `bodyWidget()` when dialog shows. + return BlockableOverlay( + underlying: bodyWidget(), + state: _blockableOverlayState, + ); + } else { + // `_blockableOverlayState` is not recreated here. + // The toolbar's block state won't work properly when reconnecting, but that's okay. + return bodyWidget(); + } + }), + ); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return WillPopScope( + onWillPop: () async { + clientClose(sessionId, _ffi.dialogManager); + return false; + }, + child: MultiProvider(providers: [ + ChangeNotifierProvider.value(value: _ffi.ffiModel), + ChangeNotifierProvider.value(value: _ffi.imageModel), + ChangeNotifierProvider.value(value: _ffi.cursorModel), + ChangeNotifierProvider.value(value: _ffi.canvasModel), + ChangeNotifierProvider.value(value: _ffi.recordingModel), + ], child: buildBody(context))); + } + + void enterView(PointerEnterEvent evt) { + _cursorOverImage.value = true; + _firstEnterImage.value = true; + if (_onEnterOrLeaveImage4Toolbar != null) { + try { + _onEnterOrLeaveImage4Toolbar!(true); + } catch (e) { + // + } + } + // See [onWindowBlur]. + if (!isWindows) { + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + _ffi.inputModel.enterOrLeave(true); + } + } + + void leaveView(PointerExitEvent evt) { + if (_ffi.ffiModel.keyboard) { + _ffi.inputModel.tryMoveEdgeOnExit(evt.position); + } + + _cursorOverImage.value = false; + _firstEnterImage.value = false; + if (_onEnterOrLeaveImage4Toolbar != null) { + try { + _onEnterOrLeaveImage4Toolbar!(false); + } catch (e) { + // + } + } + // See [onWindowBlur]. + if (!isWindows) { + _ffi.inputModel.enterOrLeave(false); + } + } + + Widget _buildRawTouchAndPointerRegion( + Widget child, + PointerEnterEventListener? onEnter, + PointerExitEventListener? onExit, + ) { + return RawTouchGestureDetectorRegion( + child: _buildRawPointerMouseRegion(child, onEnter, onExit), + ffi: _ffi, + isCamera: true, + ); + } + + Widget _buildRawPointerMouseRegion( + Widget child, + PointerEnterEventListener? onEnter, + PointerExitEventListener? onExit, + ) { + return CameraRawPointerMouseRegion( + onEnter: onEnter, + onExit: onExit, + onPointerDown: (event) { + // A double check for blur status. + // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false. + // Sometimes the system does not send the necessary focus event to flutter. We should manually + // handle this inconsistent status by setting `_isWindowBlur` to false. So we can + // ensure the grab-key thread is running when our users are clicking the remote canvas. + if (_isWindowBlur) { + debugPrint( + "Unexpected status: onPointerDown is triggered while the remote window is in blur status"); + _isWindowBlur = false; + } + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + }, + inputModel: _ffi.inputModel, + child: child, + ); + } + + Widget getBodyForDesktop(BuildContext context) { + var paints = [ + MouseRegion(onEnter: (evt) { + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false); + }, onExit: (evt) { + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true); + }, child: LayoutBuilder(builder: (context, constraints) { + final c = Provider.of(context, listen: false); + Future.delayed(Duration.zero, () => c.updateViewStyle()); + final peerDisplay = CurrentDisplayState.find(widget.id); + return Obx( + () => _ffi.ffiModel.pi.isSet.isFalse + ? Container(color: Colors.transparent) + : Obx(() { + widget.toolbarState.initShow(sessionId); + _ffi.textureModel.updateCurrentDisplay(peerDisplay.value); + return ImagePaint( + id: widget.id, + cursorOverImage: _cursorOverImage, + listenerBuilder: (child) => _buildRawTouchAndPointerRegion( + child, enterView, leaveView), + ffi: _ffi, + ); + }), + ); + })) + ]; + + paints.add( + Positioned( + top: 10, + right: 10, + child: _buildRawTouchAndPointerRegion( + QualityMonitor(_ffi.qualityMonitorModel), null, null), + ), + ); + return Stack( + children: paints, + ); + } + + @override + bool get wantKeepAlive => true; +} + +class ImagePaint extends StatefulWidget { + final FFI ffi; + final String id; + final RxBool cursorOverImage; + final Widget Function(Widget)? listenerBuilder; + + ImagePaint( + {Key? key, + required this.ffi, + required this.id, + required this.cursorOverImage, + this.listenerBuilder}) + : super(key: key); + + @override + State createState() => _ImagePaintState(); +} + +class _ImagePaintState extends State { + bool _lastRemoteCursorMoved = false; + + String get id => widget.id; + RxBool get cursorOverImage => widget.cursorOverImage; + Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder; + + @override + Widget build(BuildContext context) { + final m = Provider.of(context); + var c = Provider.of(context); + final s = c.scale; + + bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal; + + if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { + final paintWidth = c.getDisplayWidth() * s; + final paintHeight = c.getDisplayHeight() * s; + final paintSize = Size(paintWidth, paintHeight); + final paintWidget = + m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender + ? _BuildPaintTextureRender( + c, s, Offset.zero, paintSize, isViewOriginal()) + : _buildScrollbarNonTextureRender(m, paintSize, s); + return NotificationListener( + onNotification: (notification) { + c.updateScrollPercent(); + return false; + }, + child: Container( + child: _buildCrossScrollbarFromLayout( + context, + _buildListener(paintWidget), + c.size, + paintSize, + c.scrollHorizontal, + c.scrollVertical, + )), + ); + } else { + if (c.size.width > 0 && c.size.height > 0) { + final paintWidget = + m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender + ? _BuildPaintTextureRender( + c, + s, + Offset( + isLinux ? c.x.toInt().toDouble() : c.x, + isLinux ? c.y.toInt().toDouble() : c.y, + ), + c.size, + isViewOriginal()) + : _buildScrollAutoNonTextureRender(m, c, s); + return Container(child: _buildListener(paintWidget)); + } else { + return Container(); + } + } + } + + Widget _buildScrollbarNonTextureRender( + ImageModel m, Size imageSize, double s) { + return CustomPaint( + size: imageSize, + painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + ); + } + + Widget _buildScrollAutoNonTextureRender( + ImageModel m, CanvasModel c, double s) { + return CustomPaint( + size: Size(c.size.width, c.size.height), + painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + ); + } + + Widget _BuildPaintTextureRender( + CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) { + final ffiModel = c.parent.target!.ffiModel; + final displays = ffiModel.pi.getCurDisplays(); + final children = []; + final rect = ffiModel.rect; + if (rect == null) { + return Container(); + } + final curDisplay = ffiModel.pi.currentDisplay; + for (var i = 0; i < displays.length; i++) { + final textureId = widget.ffi.textureModel + .getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay); + if (true) { + // both "textureId.value != -1" and "true" seems ok + children.add(Positioned( + left: (displays[i].x - rect.left) * s + offset.dx, + top: (displays[i].y - rect.top) * s + offset.dy, + width: displays[i].width * s, + height: displays[i].height * s, + child: Obx(() => Texture( + textureId: textureId.value, + filterQuality: + isViewOriginal ? FilterQuality.none : FilterQuality.low, + )), + )); + } + } + return SizedBox( + width: size.width, + height: size.height, + child: Stack(children: children), + ); + } + + MouseCursor _buildCustomCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cache = cursor.cache ?? preDefaultCursor.cache; + return buildCursorOfCache(cursor, scale, cache); + } + + MouseCursor _buildDisabledCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cache = preForbiddenCursor.cache; + return buildCursorOfCache(cursor, scale, cache); + } + + Widget _buildCrossScrollbarFromLayout( + BuildContext context, + Widget child, + Size layoutSize, + Size size, + ScrollController horizontal, + ScrollController vertical, + ) { + var widget = child; + if (layoutSize.width < size.width) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: horizontal, + scrollDirection: Axis.horizontal, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Row( + children: [ + Container( + width: ((layoutSize.width - size.width) ~/ 2).toDouble(), + ), + widget, + ], + ); + } + if (layoutSize.height < size.height) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: vertical, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Column( + children: [ + Container( + height: ((layoutSize.height - size.height) ~/ 2).toDouble(), + ), + widget, + ], + ); + } + if (layoutSize.width < size.width) { + 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 = RawScrollbar( + thickness: kScrollbarThickness, + thumbColor: Colors.grey, + controller: vertical, + thumbVisibility: false, + trackVisibility: false, + child: widget, + ); + } + + return Container( + child: widget, + width: layoutSize.width, + height: layoutSize.height, + ); + } + + Widget _buildListener(Widget child) { + if (listenerBuilder != null) { + return listenerBuilder!(child); + } else { + return child; + } + } +} diff --git a/flutter/lib/desktop/pages/view_camera_tab_page.dart b/flutter/lib/desktop/pages/view_camera_tab_page.dart new file mode 100644 index 00000000000..4510949fa25 --- /dev/null +++ b/flutter/lib/desktop/pages/view_camera_tab_page.dart @@ -0,0 +1,499 @@ +import 'dart:convert'; +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/input_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/pages/view_camera_page.dart'; +import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:bot_toast/bot_toast.dart'; + +import '../../models/platform_model.dart'; + +class _MenuTheme { + static const Color blueColor = MyTheme.button; + // kMinInteractiveDimension + static const double height = 20.0; + static const double dividerHeight = 12.0; +} + +class ViewCameraTabPage extends StatefulWidget { + final Map params; + + const ViewCameraTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _ViewCameraTabPageState(params); +} + +class _ViewCameraTabPageState extends State { + final tabController = + Get.put(DesktopTabController(tabType: DesktopTabType.viewCamera)); + final contentKey = UniqueKey(); + static const IconData selectedIcon = Icons.desktop_windows_sharp; + static const IconData unselectedIcon = Icons.desktop_windows_outlined; + + String? peerId; + bool _isScreenRectSet = false; + int? _display; + + var connectionMap = RxList.empty(growable: true); + + _ViewCameraTabPageState(Map params) { + RemoteCountState.init(); + peerId = params['id']; + final sessionId = params['session_id']; + final tabWindowId = params['tab_window_id']; + final display = params['display']; + final displays = params['displays']; + final screenRect = parseParamScreenRect(params); + _isScreenRectSet = screenRect != null; + _display = display as int?; + tryMoveToScreenAndSetFullscreen(screenRect); + if (peerId != null) { + ConnectionTypeState.init(peerId!); + tabController.onSelected = (id) { + final viewCameraPage = tabController.widget(id); + if (viewCameraPage is ViewCameraPage) { + final ffi = viewCameraPage.ffi; + bind.setCurSessionId(sessionId: ffi.sessionId); + } + WindowController.fromWindowId(params['windowId']) + .setTitle(getWindowNameWithId(id)); + UnreadChatCountState.find(id).value = 0; + }; + tabController.add(TabInfo( + key: peerId!, + label: peerId!, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () => tabController.closeBy(peerId), + page: ViewCameraPage( + key: ValueKey(peerId), + id: peerId!, + sessionId: sessionId == null ? null : SessionID(sessionId), + tabWindowId: tabWindowId, + display: display, + displays: displays?.cast(), + password: params['password'], + toolbarState: ToolbarState(), + tabController: tabController, + connToken: params['connToken'], + forceRelay: params['forceRelay'], + isSharedPassword: params['isSharedPassword'], + ), + )); + _update_remote_count(); + } + tabController.onRemoved = (_, id) => onRemoveId(id); + rustDeskWinManager.setMethodHandler(_remoteMethodHandler); + } + + @override + void initState() { + super.initState(); + + if (!_isScreenRectSet) { + Future.delayed(Duration.zero, () { + restoreWindowPosition( + WindowType.ViewCamera, + windowId: windowId(), + peerId: tabController.state.value.tabs.isEmpty + ? null + : tabController.state.value.tabs[0].key, + display: _display, + ); + }); + } + } + + @override + Widget build(BuildContext context) { + final child = Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton(), + selectedBorderColor: MyTheme.accent, + pageViewBuilder: (pageView) => pageView, + labelGetter: DesktopTab.tablabelGetter, + tabBuilder: (key, icon, label, themeConf) => Obx(() { + final connectionType = ConnectionTypeState.find(key); + if (!connectionType.isValid()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + label, + ], + ); + } else { + bool secure = + connectionType.secure.value == ConnectionType.strSecure; + bool direct = + connectionType.direct.value == ConnectionType.strDirect; + String msgConn; + if (secure && direct) { + msgConn = translate("Direct and encrypted connection"); + } else if (secure && !direct) { + msgConn = translate("Relayed and encrypted connection"); + } else if (!secure && direct) { + msgConn = translate("Direct and unencrypted connection"); + } else { + msgConn = translate("Relayed and unencrypted connection"); + } + var msgFingerprint = '${translate('Fingerprint')}:\n'; + var fingerprint = FingerprintState.find(key).value; + if (fingerprint.isEmpty) { + fingerprint = 'N/A'; + } + if (fingerprint.length > 5 * 8) { + var first = fingerprint.substring(0, 39); + var second = fingerprint.substring(40); + msgFingerprint += '$first\n$second'; + } else { + msgFingerprint += fingerprint; + } + + final tab = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: '$msgConn\n$msgFingerprint', + child: SvgPicture.asset( + 'assets/${connectionType.secure.value}${connectionType.direct.value}.svg', + width: themeConf.iconSize, + height: themeConf.iconSize, + ).paddingOnly(right: 5), + ), + label, + unreadMessageCountBuilder(UnreadChatCountState.find(key)) + .marginOnly(left: 4), + ], + ); + + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + final viewCameraPage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as ViewCameraPage; + if (viewCameraPage.ffi.ffiModel.pi.isSet.isTrue && + e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return _tabMenuBuilder(key, cancelFunc); + }, + target: e.position, + ); + } + }, + child: tab, + ); + } + }), + ), + ); + final tabWidget = isLinux + ? buildVirtualWindowFrame(context, child) + : workaroundWindowBorder( + context, + Obx(() => Container( + decoration: BoxDecoration( + border: Border.all( + color: MyTheme.color(context).border!, + width: stateGlobal.windowBorderWidth.value), + ), + child: child, + ))); + return isMacOS || kUseCompatibleUiMode + ? tabWidget + : Obx(() => SubWindowDragToResizeArea( + key: contentKey, + child: tabWidget, + // Specially configured for a better resize area and remote control. + childPadding: kDragToResizeAreaPadding, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, + windowId: stateGlobal.windowId, + )); + } + + // Note: Some dup code to ../widgets/remote_toolbar + Widget _tabMenuBuilder(String key, CancelFunc cancelFunc) { + final List> menu = []; + const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0); + final viewCameraPage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as ViewCameraPage; + final ffi = viewCameraPage.ffi; + final sessionId = ffi.sessionId; + final toolbarState = viewCameraPage.toolbarState; + menu.addAll([ + MenuEntryButton( + childBuilder: (TextStyle? style) => Obx(() => Text( + translate( + toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'), + style: style, + )), + proc: () { + toolbarState.switchShow(sessionId); + cancelFunc(); + }, + padding: padding, + ), + ]); + + if (tabController.state.value.tabs.length > 1) { + final splitAction = MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Move tab to new window'), + style: style, + ), + proc: () async { + await DesktopMultiWindow.invokeMethod( + kMainWindowId, + kWindowEventMoveTabToNewWindow, + '${windowId()},$key,$sessionId,ViewCamera'); + cancelFunc(); + }, + padding: padding, + ); + menu.insert(1, splitAction); + } + + menu.addAll([ + MenuEntryDivider(), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Copy Fingerprint'), + style: style, + ), + proc: () => onCopyFingerprint(FingerprintState.find(key).value), + padding: padding, + dismissOnClicked: true, + dismissCallback: cancelFunc, + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Close'), + style: style, + ), + proc: () { + tabController.closeBy(key); + cancelFunc(); + }, + padding: padding, + ) + ]); + + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenuTheme.blueColor, + height: _MenuTheme.height, + dividerHeight: _MenuTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } + + void onRemoveId(String id) async { + if (tabController.state.value.tabs.isEmpty) { + // Keep calling until the window status is hidden. + // + // Workaround for Windows: + // If you click other buttons and close in msgbox within a very short period of time, the close may fail. + // `await WindowController.fromWindowId(windowId()).close();`. + Future loopCloseWindow() async { + int c = 0; + final windowController = WindowController.fromWindowId(windowId()); + while (c < 20 && + tabController.state.value.tabs.isEmpty && + (!await windowController.isHidden())) { + await windowController.close(); + await Future.delayed(Duration(milliseconds: 100)); + c++; + } + } + + loopCloseWindow(); + } + ConnectionTypeState.delete(id); + _update_remote_count(); + } + + int windowId() { + return widget.params["windowId"]; + } + + Future handleWindowCloseButton() async { + final connLength = tabController.length; + if (connLength <= 1) { + tabController.clear(); + return true; + } else { + final bool res; + if (!option2bool(kOptionEnableConfirmClosingTabs, + bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + tabController.clear(); + } + return res; + } + } + + _update_remote_count() => + RemoteCountState.find().value = tabController.length; + + Future _remoteMethodHandler(call, fromWindowId) async { + debugPrint( + "[View Camera Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + + dynamic returnValue; + // for simplify, just replace connectionId + if (call.method == kWindowEventNewViewCamera) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final sessionId = args['session_id']; + final tabWindowId = args['tab_window_id']; + final display = args['display']; + final displays = args['displays']; + final screenRect = parseParamScreenRect(args); + final prePeerCount = tabController.length; + Future.delayed(Duration.zero, () async { + if (stateGlobal.fullscreen.isTrue) { + await WindowController.fromWindowId(windowId()).setFullscreen(false); + stateGlobal.setFullscreen(false, procWnd: false); + } + await setNewConnectWindowFrame(windowId(), id!, prePeerCount, + WindowType.ViewCamera, display, screenRect); + Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async { + await windowOnTop(windowId()); + }); + }); + ConnectionTypeState.init(id); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () => tabController.closeBy(id), + page: ViewCameraPage( + key: ValueKey(id), + id: id, + sessionId: sessionId == null ? null : SessionID(sessionId), + tabWindowId: tabWindowId, + display: display, + displays: displays?.cast(), + password: args['password'], + toolbarState: ToolbarState(), + tabController: tabController, + connToken: args['connToken'], + forceRelay: args['forceRelay'], + isSharedPassword: args['isSharedPassword'], + ), + )); + } else if (call.method == kWindowDisableGrabKeyboard) { + // ??? + } else if (call.method == "onDestroy") { + tabController.clear(); + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } else if (call.method == kWindowEventActiveSession) { + final jumpOk = tabController.jumpToByKey(call.arguments); + if (jumpOk) { + windowOnTop(windowId()); + } + return jumpOk; + } else if (call.method == kWindowEventActiveDisplaySession) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final display = args['display']; + final jumpOk = + tabController.jumpToByKeyAndDisplay(id, display, isCamera: true); + if (jumpOk) { + windowOnTop(windowId()); + } + return jumpOk; + } else if (call.method == kWindowEventGetRemoteList) { + return tabController.state.value.tabs + .map((e) => e.key) + .toList() + .join(','); + } else if (call.method == kWindowEventGetSessionIdList) { + return tabController.state.value.tabs + .map((e) => '${e.key},${(e.page as ViewCameraPage).ffi.sessionId}') + .toList() + .join(';'); + } else if (call.method == kWindowEventGetCachedSessionData) { + // Ready to show new window and close old tab. + final args = jsonDecode(call.arguments); + final id = args['id']; + final close = args['close']; + try { + final viewCameraPage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == id) + .page as ViewCameraPage; + returnValue = viewCameraPage.ffi.ffiModel.cachedPeerData.toString(); + } catch (e) { + debugPrint('Failed to get cached session data: $e'); + } + if (close && returnValue != null) { + closeSessionOnDispose[id] = false; + tabController.closeBy(id); + } + } else if (call.method == kWindowEventRemoteWindowCoords) { + final viewCameraPage = + tabController.state.value.selectedTabInfo.page as ViewCameraPage; + final ffi = viewCameraPage.ffi; + final displayRect = ffi.ffiModel.displaysRect(); + if (displayRect != null) { + final wc = WindowController.fromWindowId(windowId()); + Rect? frame; + try { + frame = await wc.getFrame(); + } catch (e) { + debugPrint( + "Failed to get frame of window $windowId, it may be hidden"); + } + if (frame != null) { + ffi.cursorModel.moveLocal(0, 0); + final coords = RemoteWindowCoords( + frame, + CanvasCoords.fromCanvasModel(ffi.canvasModel), + CursorCoords.fromCursorModel(ffi.cursorModel), + displayRect); + returnValue = jsonEncode(coords.toJson()); + } + } + } else if (call.method == kWindowEventSetFullscreen) { + stateGlobal.setFullscreen(call.arguments == 'true'); + } + _update_remote_count(); + return returnValue; + } +} diff --git a/flutter/lib/desktop/screen/desktop_view_camera_screen.dart b/flutter/lib/desktop/screen/desktop_view_camera_screen.dart new file mode 100644 index 00000000000..a845b89d01c --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_view_camera_screen.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/view_camera_tab_page.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab desktop remote screen +class DesktopViewCameraScreen extends StatelessWidget { + final Map params; + + DesktopViewCameraScreen({Key? key, required this.params}) : super(key: key) { + bind.mainInitInputSource(); + stateGlobal.getInputSource(force: true); + } + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ChangeNotifierProvider.value(value: gFFI.imageModel), + ChangeNotifierProvider.value(value: gFFI.cursorModel), + ChangeNotifierProvider.value(value: gFFI.canvasModel), + ], + child: Scaffold( + // Set transparent background for padding the resize area out of the flutter view. + // This allows the wallpaper goes through our resize area. (Linux only now). + backgroundColor: isLinux ? Colors.transparent : null, + body: ViewCameraTabPage( + params: params, + ), + )); + } +} diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index d826ea8c6b6..31b8b2263e7 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -478,7 +478,10 @@ class _RemoteToolbarState extends State { state: widget.state, setFullscreen: _setFullscreen, )); - toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); + // Do not show keyboard for camera connection type. + if (widget.ffi.connType == ConnType.defaultConn) { + toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); + } toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); if (!isWeb) { toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); @@ -1043,23 +1046,26 @@ class _DisplayMenuState extends State<_DisplayMenu> { scrollStyle(), imageQuality(), codec(), - _ResolutionsMenu( - id: widget.id, - ffi: widget.ffi, - screenAdjustor: _screenAdjustor, - ), - if (showVirtualDisplayMenu(ffi)) + if (ffi.connType == ConnType.defaultConn) + _ResolutionsMenu( + id: widget.id, + ffi: widget.ffi, + screenAdjustor: _screenAdjustor, + ), + if (showVirtualDisplayMenu(ffi) && ffi.connType == ConnType.defaultConn) _SubmenuButton( ffi: widget.ffi, menuChildren: getVirtualDisplayMenuChildren(ffi, id, null), child: Text(translate("Virtual display")), ), - cursorToggles(), + if (ffi.connType == ConnType.defaultConn) cursorToggles(), Divider(), toggles(), ]; // privacy mode - if (ffiModel.keyboard && pi.features.privacyMode) { + if (ffi.connType == ConnType.defaultConn && + ffiModel.keyboard && + pi.features.privacyMode) { final privacyModeState = PrivacyModeState.find(id); final privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, ffi); @@ -1085,7 +1091,9 @@ class _DisplayMenuState extends State<_DisplayMenu> { ]); } } - menuChildren.add(widget.pluginItem); + if (ffi.connType == ConnType.defaultConn) { + menuChildren.add(widget.pluginItem); + } return menuChildren; } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 96ada22c907..c8640e05fba 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart' hide TabBarTheme; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; +import 'package:flutter_hbb/desktop/pages/view_camera_page.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -51,6 +52,7 @@ enum DesktopTabType { cm, remoteScreen, fileTransfer, + viewCamera, portForward, install, } @@ -179,11 +181,13 @@ class DesktopTabController { jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key), callOnSelected: callOnSelected); - bool jumpToByKeyAndDisplay(String key, int display) { + bool jumpToByKeyAndDisplay(String key, int display, {bool isCamera = false}) { for (int i = 0; i < state.value.tabs.length; i++) { final tab = state.value.tabs[i]; if (tab.key == key) { - final ffi = (tab.page as RemotePage).ffi; + final ffi = isCamera + ? (tab.page as ViewCameraPage).ffi + : (tab.page as RemotePage).ffi; if (ffi.ffiModel.pi.currentDisplay == display) { return jumpTo(i, callOnSelected: true); } @@ -725,6 +729,7 @@ class WindowActionPanelState extends State { return widget.tabController.state.value.tabs.length > 1 && (widget.tabController.tabType == DesktopTabType.remoteScreen || widget.tabController.tabType == DesktopTabType.fileTransfer || + widget.tabController.tabType == DesktopTabType.viewCamera || widget.tabController.tabType == DesktopTabType.portForward || widget.tabController.tabType == DesktopTabType.cm); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index b5a0af71144..f04e142d94b 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -11,6 +11,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/install_page.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; +import 'package:flutter_hbb/desktop/screen/desktop_view_camera_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; @@ -76,6 +77,13 @@ Future main(List args) async { kAppTypeDesktopFileTransfer, ); break; + case WindowType.ViewCamera: + desktopType = DesktopType.viewCamera; + runMultiWindow( + argument, + kAppTypeDesktopViewCamera, + ); + break; case WindowType.PortForward: desktopType = DesktopType.portForward; runMultiWindow( @@ -192,6 +200,12 @@ void runMultiWindow( params: argument, ); break; + case kAppTypeDesktopViewCamera: + draggablePositions.load(); + widget = DesktopViewCameraScreen( + params: argument, + ); + break; case kAppTypeDesktopPortForward: widget = DesktopPortForwardScreen( params: argument, @@ -227,6 +241,19 @@ void runMultiWindow( await restoreWindowPosition(WindowType.FileTransfer, windowId: kWindowId!); break; + case kAppTypeDesktopViewCamera: + // If screen rect is set, the window will be moved to the target screen and then set fullscreen. + if (argument['screen_rect'] == null) { + // display can be used to control the offset of the window. + await restoreWindowPosition( + WindowType.ViewCamera, + windowId: kWindowId!, + peerId: argument['id'] as String?, + // FIXME: fix display index. + display: argument['display'] as int?, + ); + } + break; case kAppTypeDesktopPortForward: await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!); break; diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index efccc5de65e..82d7058ab50 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -204,6 +204,7 @@ class WebHomePage extends StatelessWidget { return; } bool isFileTransfer = false; + bool isViewCamera = false; String? id; String? password; for (int i = 0; i < args.length; i++) { @@ -219,6 +220,11 @@ class WebHomePage extends StatelessWidget { id = args[i + 1]; i++; break; + case '--view-camera': + isViewCamera = true; + id = args[i + 1]; + i++; + break; case '--password': password = args[i + 1]; i++; @@ -228,7 +234,7 @@ class WebHomePage extends StatelessWidget { } } if (id != null) { - connect(context, id, isFileTransfer: isFileTransfer, password: password); + connect(context, id, isFileTransfer: isFileTransfer, isViewCamera: isViewCamera, password: password); } } } diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart new file mode 100644 index 00000000000..afd24dc7e03 --- /dev/null +++ b/flutter/lib/mobile/pages/view_camera_page.dart @@ -0,0 +1,721 @@ +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/common/widgets/toolbar.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + +import '../../common.dart'; +import '../../common/widgets/overlay.dart'; +import '../../common/widgets/dialog.dart'; +import '../../common/widgets/remote_input.dart'; +import '../../models/input_model.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import '../../utils/image.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 ViewCameraPage extends StatefulWidget { + ViewCameraPage( + {Key? key, required this.id, this.password, this.isSharedPassword}) + : super(key: key); + + final String id; + final String? password; + final bool? isSharedPassword; + + @override + State createState() => _ViewCameraPageState(id); +} + +class _ViewCameraPageState extends State + with WidgetsBindingObserver { + Timer? _timer; + bool _showBar = !isWebDesktop; + bool _showGestureHelp = false; + Orientation? _currentOrientation; + double _viewInsetsBottom = 0; + + Timer? _timerDidChangeMetrics; + + final _blockableOverlayState = BlockableOverlayState(); + + final keyboardVisibilityController = KeyboardVisibilityController(); + final FocusNode _mobileFocusNode = FocusNode(); + final FocusNode _physicalFocusNode = FocusNode(); + var _showEdit = false; // use soft keyboard + + InputModel get inputModel => gFFI.inputModel; + SessionID get sessionId => gFFI.sessionId; + + final TextEditingController _textController = + TextEditingController(text: initText); + + _ViewCameraPageState(String id) { + initSharedStates(id); + gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted; + gFFI.dialogManager.loadMobileActionsOverlayVisible(); + } + + @override + void initState() { + super.initState(); + gFFI.ffiModel.updateEventListener(sessionId, widget.id); + gFFI.start( + widget.id, + isViewCamera: true, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + gFFI.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + if (!isWeb) { + WakelockPlus.enable(); + } + _physicalFocusNode.requestFocus(); + gFFI.inputModel.listenToMouse(true); + gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId); + 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')); + } + _disableAndroidSoftKeyboard( + isKeyboardVisible: keyboardVisibilityController.isVisible); + }); + 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); + gFFI.inputModel.listenToMouse(false); + gFFI.imageModel.disposeImage(); + gFFI.cursorModel.disposeImages(); + await gFFI.invokeMethod("enable_soft_keyboard", true); + _mobileFocusNode.dispose(); + _physicalFocusNode.dispose(); + await gFFI.close(); + _timer?.cancel(); + _timerDidChangeMetrics?.cancel(); + gFFI.dialogManager.dismissAll(); + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + if (!isWeb) { + await WakelockPlus.disable(); + } + removeSharedStates(widget.id); + // `on_voice_call_closed` should be called when the connection is ended. + // 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"); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) {} + + @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 { + // We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`. + if (newBottom != _viewInsetsBottom) { + gFFI.canvasModel.mobileFocusCanvasCursor(); + _viewInsetsBottom = newBottom; + } + }); + } + + // 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. + // But I don't know why and how to fix it. + Widget emptyOverlay(Color bgColor) => BlockableOverlay( + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] + state: _blockableOverlayState, + underlying: Container( + color: bgColor, + ), + ); + + Widget _bottomWidget() => (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty + ? getBottomAppBar() + : Offstage()); + + @override + Widget build(BuildContext context) { + final keyboardIsVisible = + keyboardVisibilityController.isVisible && _showEdit; + final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp; + + return WillPopScope( + onWillPop: () async { + clientClose(sessionId, gFFI.dialogManager); + return false; + }, + child: Scaffold( + // workaround for https://github.com/rustdesk/rustdesk/issues/3131 + floatingActionButtonLocation: keyboardIsVisible + ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) + : null, + floatingActionButton: !showActionButton + ? null + : FloatingActionButton( + mini: !keyboardIsVisible, + child: Icon( + (keyboardIsVisible || _showGestureHelp) + ? Icons.expand_more + : Icons.expand_less, + color: Colors.white, + ), + backgroundColor: MyTheme.accent, + onPressed: () { + setState(() { + if (keyboardIsVisible) { + _showEdit = false; + gFFI.invokeMethod("enable_soft_keyboard", false); + _mobileFocusNode.unfocus(); + _physicalFocusNode.requestFocus(); + } else if (_showGestureHelp) { + _showGestureHelp = false; + } else { + _showBar = !_showBar; + } + }); + }), + bottomNavigationBar: Obx(() => Stack( + alignment: Alignment.bottomCenter, + children: [ + gFFI.ffiModel.pi.isSet.isTrue && + gFFI.ffiModel.waitForFirstImage.isTrue + ? emptyOverlay(MyTheme.canvasColor) + : () { + gFFI.ffiModel.tryShowAndroidActionsOverlay(); + return Offstage(); + }(), + _bottomWidget(), + gFFI.ffiModel.pi.isSet.isFalse + ? emptyOverlay(MyTheme.canvasColor) + : Offstage(), + ], + )), + body: Obx( + () => getRawPointerAndKeyBody(Overlay( + initialEntries: [ + OverlayEntry(builder: (context) { + return Container( + color: kColorCanvas, + child: SafeArea( + child: OrientationBuilder(builder: (ctx, orientation) { + if (_currentOrientation != orientation) { + Timer(const Duration(milliseconds: 200), () { + gFFI.dialogManager + .resetMobileActionsOverlay(ffi: gFFI); + _currentOrientation = orientation; + gFFI.canvasModel.updateViewStyle(); + }); + } + return Container( + color: MyTheme.canvasColor, + child: inputModel.isPhysicalMouse.value + ? getBodyForMobile() + : RawTouchGestureDetectorRegion( + child: getBodyForMobile(), + ffi: gFFI, + isCamera: true, + ), + ); + }), + ), + ); + }) + ], + )), + )), + ); + } + + Widget getRawPointerAndKeyBody(Widget child) { + return CameraRawPointerMouseRegion( + inputModel: inputModel, + // Disable RawKeyFocusScope before the connecting is established. + // The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog. + child: gFFI.ffiModel.pi.isSet.isTrue + ? RawKeyFocusScope( + focusNode: _physicalFocusNode, + inputModel: inputModel, + child: child) + : child, + ); + } + + Widget getBottomAppBar() { + return BottomAppBar( + elevation: 10, + color: MyTheme.accent, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + color: Colors.white, + icon: Icon(Icons.clear), + onPressed: () { + clientClose(sessionId, gFFI.dialogManager); + }, + ), + IconButton( + color: Colors.white, + icon: Icon(Icons.tv), + onPressed: () { + setState(() => _showEdit = false); + showOptions(context, widget.id, gFFI.dialogManager); + }, + ) + ] + + (isWeb + ? [] + : [ + futureBuilder( + future: gFFI.invokeMethod( + "get_value", "KEY_IS_SUPPORT_VOICE_CALL"), + hasData: (isSupportVoiceCall) => IconButton( + color: Colors.white, + icon: isAndroid && isSupportVoiceCall + ? SvgPicture.asset('assets/chat.svg', + colorFilter: ColorFilter.mode( + Colors.white, BlendMode.srcIn)) + : Icon(Icons.message), + onPressed: () => + isAndroid && isSupportVoiceCall + ? showChatOptions(widget.id) + : onPressedTextChat(widget.id), + )) + ]) + + [ + IconButton( + color: Colors.white, + icon: Icon(Icons.more_vert), + onPressed: () { + setState(() => _showEdit = false); + showActions(widget.id); + }, + ), + ]), + Obx(() => IconButton( + color: Colors.white, + icon: Icon(Icons.expand_more), + onPressed: gFFI.ffiModel.waitForFirstImage.isTrue + ? null + : () { + setState(() => _showBar = !_showBar); + }, + )), + ], + ), + ); + } + + Widget getBodyForMobile() { + return Container( + color: MyTheme.canvasColor, + child: Stack(children: () { + final paints = [ + ImagePaint(), + Positioned( + top: 10, + right: 10, + child: QualityMonitor(gFFI.qualityMonitorModel), + ), + SizedBox( + width: 0, + height: 0, + child: !_showEdit + ? Container() + : TextFormField( + textInputAction: TextInputAction.newline, + autocorrect: false, + // Flutter 3.16.9 Android. + // `enableSuggestions` causes secure keyboard to be shown. + // https://github.com/flutter/flutter/issues/139143 + // https://github.com/flutter/flutter/issues/146540 + // enableSuggestions: false, + autofocus: true, + focusNode: _mobileFocusNode, + maxLines: null, + 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: null, + ).workaroundFreezeLinuxMint(), + ), + ]; + return paints; + }())); + } + + Widget getBodyForDesktopWithListener() { + var paints = [ImagePaint()]; + return Container( + color: MyTheme.canvasColor, child: Stack(children: paints)); + } + + List _getMobileActionMenus() { + if (gFFI.ffiModel.pi.platform != kPeerPlatformAndroid || + !gFFI.ffiModel.keyboard) { + return []; + } + final enabled = versionCmp(gFFI.ffiModel.pi.version, '1.2.7') >= 0; + if (!enabled) return []; + return [ + TTextMenu( + child: Text(translate('Back')), + onPressed: () => gFFI.inputModel.onMobileBack(), + ), + TTextMenu( + child: Text(translate('Home')), + onPressed: () => gFFI.inputModel.onMobileHome(), + ), + TTextMenu( + child: Text(translate('Apps')), + onPressed: () => gFFI.inputModel.onMobileApps(), + ), + TTextMenu( + child: Text(translate('Volume up')), + onPressed: () => gFFI.inputModel.onMobileVolumeUp(), + ), + TTextMenu( + child: Text(translate('Volume down')), + onPressed: () => gFFI.inputModel.onMobileVolumeDown(), + ), + TTextMenu( + child: Text(translate('Power')), + onPressed: () => gFFI.inputModel.onMobilePower(), + ), + ]; + } + + void showActions(String id) async { + final size = MediaQuery.of(context).size; + final x = 120.0; + final y = size.height; + final mobileActionMenus = _getMobileActionMenus(); + final menus = toolbarControls(context, id, gFFI); + + final List> more = [ + ...mobileActionMenus + .asMap() + .entries + .map((e) => + PopupMenuItem(child: e.value.getChild(), value: e.key)) + .toList(), + if (mobileActionMenus.isNotEmpty) PopupMenuDivider(), + ...menus + .asMap() + .entries + .map((e) => PopupMenuItem( + child: e.value.getChild(), + value: e.key + mobileActionMenus.length)) + .toList(), + ]; + () async { + var index = await showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + items: more, + elevation: 8, + ); + if (index != null) { + if (index < mobileActionMenus.length) { + mobileActionMenus[index].onPressed.call(); + } else if (index < mobileActionMenus.length + more.length) { + menus[index - mobileActionMenus.length].onPressed.call(); + } + } + }(); + } + + onPressedTextChat(String id) { + gFFI.chatModel.changeCurrentKey(MessageKey(id, ChatModel.clientModeID)); + gFFI.chatModel.toggleChatOverlay(); + } + + showChatOptions(String id) async { + onPressVoiceCall() => bind.sessionRequestVoiceCall(sessionId: sessionId); + onPressEndVoiceCall() => bind.sessionCloseVoiceCall(sessionId: sessionId); + + makeTextMenu(String label, Widget icon, VoidCallback onPressed, + {TextStyle? labelStyle}) => + TTextMenu( + child: Text(translate(label), style: labelStyle), + trailingIcon: Transform.scale( + scale: (isDesktop || isWebDesktop) ? 0.8 : 1, + child: IgnorePointer( + child: IconButton( + onPressed: null, + icon: icon, + ), + ), + ), + onPressed: onPressed, + ); + + final isInVoice = [ + VoiceCallStatus.waitingForResponse, + VoiceCallStatus.connected + ].contains(gFFI.chatModel.voiceCallStatus.value); + final menus = [ + makeTextMenu('Text chat', Icon(Icons.message, color: MyTheme.accent), + () => onPressedTextChat(widget.id)), + isInVoice + ? makeTextMenu( + 'End voice call', + SvgPicture.asset( + 'assets/call_wait.svg', + colorFilter: + ColorFilter.mode(Colors.redAccent, BlendMode.srcIn), + ), + onPressEndVoiceCall, + labelStyle: TextStyle(color: Colors.redAccent)) + : makeTextMenu( + 'Voice call', + SvgPicture.asset( + 'assets/call_wait.svg', + colorFilter: ColorFilter.mode(MyTheme.accent, BlendMode.srcIn), + ), + onPressVoiceCall), + ]; + + final menuItems = menus + .asMap() + .entries + .map((e) => PopupMenuItem(child: e.value.getChild(), value: e.key)) + .toList(); + Future.delayed(Duration.zero, () async { + final size = MediaQuery.of(context).size; + final x = 120.0; + final y = size.height; + var index = await showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + items: menuItems, + elevation: 8, + ); + if (index != null && index < menus.length) { + menus[index].onPressed.call(); + } + }); + } +} + +class ImagePaint extends StatelessWidget { + @override + Widget build(BuildContext context) { + 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 + adjust) / s, scale: s), + ); + } +} + +void showOptions( + BuildContext context, String id, OverlayDialogManager dialogManager) async { + var displays = []; + final pi = gFFI.ffiModel.pi; + final image = gFFI.ffiModel.getConnectionImage(); + if (image != null) { + displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); + } + if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) { + final cur = pi.currentDisplay; + final children = []; + for (var i = 0; i < pi.displays.length; ++i) { + children.add(InkWell( + onTap: () { + if (i == cur) return; + openMonitorInTheSameTab(i, gFFI, pi); + gFFI.dialogManager.dismissAll(); + }, + child: Ink( + width: 40, + height: 40, + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).hintColor), + borderRadius: BorderRadius.circular(2), + color: i == cur + ? Theme.of(context).primaryColor.withOpacity(0.6) + : null), + child: Center( + child: Text((i + 1).toString(), + style: TextStyle( + color: i == cur ? Colors.white : Colors.black87, + fontWeight: FontWeight.bold)))))); + } + displays.add(Padding( + padding: const EdgeInsets.only(top: 8), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 8, + children: children, + ))); + } + if (displays.isNotEmpty) { + displays.add(const Divider(color: MyTheme.border)); + } + + List> viewStyleRadios = + await toolbarViewStyle(context, id, gFFI); + List> imageQualityRadios = + await toolbarImageQuality(context, id, gFFI); + List> codecRadios = await toolbarCodec(context, id, gFFI); + List displayToggles = + await toolbarDisplayToggle(context, id, gFFI); + + dialogManager.show((setState, close, context) { + var viewStyle = + (viewStyleRadios.isNotEmpty ? viewStyleRadios[0].groupValue : '').obs; + var imageQuality = + (imageQualityRadios.isNotEmpty ? imageQualityRadios[0].groupValue : '') + .obs; + var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs; + final radios = [ + for (var e in viewStyleRadios) + Obx(() => getRadio( + e.child, + e.value, + viewStyle.value, + e.onChanged != null + ? (v) { + e.onChanged?.call(v); + if (v != null) viewStyle.value = v; + } + : null)), + const Divider(color: MyTheme.border), + for (var e in imageQualityRadios) + Obx(() => getRadio( + e.child, + e.value, + imageQuality.value, + e.onChanged != null + ? (v) { + e.onChanged?.call(v); + if (v != null) imageQuality.value = v; + } + : null)), + const Divider(color: MyTheme.border), + for (var e in codecRadios) + Obx(() => getRadio( + e.child, + e.value, + codec.value, + e.onChanged != null + ? (v) { + e.onChanged?.call(v); + if (v != null) codec.value = v; + } + : null)), + if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border), + ]; + + final rxToggleValues = displayToggles.map((e) => e.value.obs).toList(); + final displayTogglesList = displayToggles + .asMap() + .entries + .map((e) => Obx(() => CheckboxListTile( + contentPadding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + value: rxToggleValues[e.key].value, + onChanged: e.value.onChanged != null + ? (v) { + e.value.onChanged?.call(v); + if (v != null) rxToggleValues[e.key].value = v; + } + : null, + title: e.value.child))) + .toList(); + final toggles = [ + ...displayTogglesList, + ]; + + var popupDialogMenus = List.empty(growable: true); + if (popupDialogMenus.isNotEmpty) { + popupDialogMenus.add(const Divider(color: MyTheme.border)); + } + + return CustomAlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: displays + radios + popupDialogMenus + toggles), + ); + }, clickMaskDismiss: true, backDismiss: true).then((value) { + _disableAndroidSoftKeyboard(); + }); +} + +class FABLocation extends FloatingActionButtonLocation { + FloatingActionButtonLocation location; + double offsetX; + double offsetY; + FABLocation(this.location, this.offsetX, this.offsetY); + + @override + Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { + final offset = location.getOffset(scaffoldGeometry); + return Offset(offset.dx + offsetX, offset.dy + offsetY); + } +} diff --git a/flutter/lib/models/desktop_render_texture.dart b/flutter/lib/models/desktop_render_texture.dart index c6cf55256de..a960491346f 100644 --- a/flutter/lib/models/desktop_render_texture.dart +++ b/flutter/lib/models/desktop_render_texture.dart @@ -235,6 +235,17 @@ class TextureModel { } } + onViewCameraPageDispose(bool closeSession) async { + final ffi = parent.target; + if (ffi == null) return; + for (final texture in _pixelbufferRenderTextures.values) { + await texture.destroy(closeSession, ffi); + } + for (final texture in _gpuRenderTextures.values) { + await texture.destroy(closeSession, ffi); + } + } + ensureControl(int display) { var ctl = _control[display]; if (ctl == null) { diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 2e7a36fc15b..463d5a32b71 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -369,6 +369,7 @@ class InputModel { String? get peerPlatform => parent.target?.ffiModel.pi.platform; bool get isViewOnly => parent.target!.ffiModel.viewOnly; double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio; + bool get isViewCamera => parent.target!.connType == ConnType.viewCamera; InputModel(this.parent) { sessionId = parent.target!.sessionId; @@ -471,6 +472,7 @@ class InputModel { KeyEventResult handleRawKeyEvent(RawKeyEvent e) { if (isViewOnly) return KeyEventResult.handled; + if (isViewCamera) return KeyEventResult.handled; if (!isInputSourceFlutter) { if (isDesktop) { return KeyEventResult.handled; @@ -525,6 +527,7 @@ class InputModel { KeyEventResult handleKeyEvent(KeyEvent e) { if (isViewOnly) return KeyEventResult.handled; + if (isViewCamera) return KeyEventResult.handled; if (!isInputSourceFlutter) { if (isDesktop) { return KeyEventResult.handled; @@ -724,6 +727,7 @@ class InputModel { /// [press] indicates a click event(down and up). void inputKey(String name, {bool? down, bool? press}) { if (!keyboardPerm) return; + if (isViewCamera) return; bind.sessionInputKey( sessionId: sessionId, name: name, @@ -785,6 +789,7 @@ class InputModel { /// Send scroll event with scroll distance [y]. Future scroll(int y) async { + if (isViewCamera) return; await bind.sessionSendMouse( sessionId: sessionId, msg: json @@ -808,6 +813,7 @@ class InputModel { /// Send mouse press event. Future sendMouse(String type, MouseButtons button) async { if (!keyboardPerm) return; + if (isViewCamera) return; await bind.sessionSendMouse( sessionId: sessionId, msg: json.encode(modify({'type': type, 'buttons': button.value}))); @@ -834,6 +840,7 @@ class InputModel { /// Send mouse movement event with distance in [x] and [y]. Future moveMouse(double x, double y) async { if (!keyboardPerm) return; + if (isViewCamera) return; var x2 = x.toInt(); var y2 = y.toInt(); await bind.sessionSendMouse( @@ -857,6 +864,7 @@ class InputModel { _lastScale = 1.0; _stopFling = true; if (isViewOnly) return; + if (isViewCamera) return; if (peerPlatform == kPeerPlatformAndroid) { handlePointerEvent('touch', kMouseEventTypePanStart, e.position); } @@ -865,6 +873,7 @@ class InputModel { // https://docs.flutter.dev/release/breaking-changes/trackpad-gestures void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) { if (isViewOnly) return; + if (isViewCamera) return; if (peerPlatform != kPeerPlatformAndroid) { final scale = ((e.scale - _lastScale) * 1000).toInt(); _lastScale = e.scale; @@ -904,6 +913,7 @@ class InputModel { handlePointerEvent('touch', kMouseEventTypePanUpdate, Offset(x.toDouble(), y.toDouble())); } else { + if (isViewCamera) return; bind.sessionSendMouse( sessionId: sessionId, msg: '{"type": "trackpad", "x": "$x", "y": "$y"}'); @@ -912,6 +922,7 @@ class InputModel { } void _scheduleFling(double x, double y, int delay) { + if (isViewCamera) return; if ((x == 0 && y == 0) || _stopFling) { _fling = false; return; @@ -963,6 +974,7 @@ class InputModel { } void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { + if (isViewCamera) return; if (peerPlatform == kPeerPlatformAndroid) { handlePointerEvent('touch', kMouseEventTypePanEnd, e.position); return; @@ -994,6 +1006,7 @@ class InputModel { _remoteWindowCoords = []; _windowRect = null; if (isViewOnly) return; + if (isViewCamera) return; if (e.kind != ui.PointerDeviceKind.mouse) { if (isPhysicalMouse.value) { isPhysicalMouse.value = false; @@ -1007,6 +1020,7 @@ class InputModel { void onPointUpImage(PointerUpEvent e) { if (isDesktop) _queryOtherWindowCoords = false; if (isViewOnly) return; + if (isViewCamera) return; if (e.kind != ui.PointerDeviceKind.mouse) return; if (isPhysicalMouse.value) { handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position); @@ -1015,6 +1029,7 @@ class InputModel { void onPointMoveImage(PointerMoveEvent e) { if (isViewOnly) return; + if (isViewCamera) return; if (e.kind != ui.PointerDeviceKind.mouse) return; if (_queryOtherWindowCoords) { Future.delayed(Duration.zero, () async { @@ -1049,6 +1064,7 @@ class InputModel { void onPointerSignalImage(PointerSignalEvent e) { if (isViewOnly) return; + if (isViewCamera) return; if (e is PointerScrollEvent) { var dx = e.scrollDelta.dx.toInt(); var dy = e.scrollDelta.dy.toInt(); @@ -1146,6 +1162,7 @@ class InputModel { } final evt = PointerEventToRust(kind, type, evtValue).toJson(); + if (isViewCamera) return; bind.sessionSendPointer( sessionId: sessionId, msg: json.encode(modify(evt))); } @@ -1177,6 +1194,7 @@ class InputModel { Offset offset, { bool onExit = false, }) { + if (isViewCamera) return; double x = offset.dx; double y = max(0.0, offset.dy); if (_checkPeerControlProtected(x, y)) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e408e0e2fc2..dd7927abcf4 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -407,7 +407,9 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.sendEmptyDirs(evt); } } else if (name == "record_status") { - if (desktopType == DesktopType.remote || isMobile) { + if (desktopType == DesktopType.remote || + desktopType == DesktopType.viewCamera || + isMobile) { parent.target?.recordingModel.updateStatus(evt['start'] == 'true'); } } else { @@ -501,7 +503,9 @@ class FfiModel with ChangeNotifier { final display = int.parse(evt['display']); if (_pi.currentDisplay != kAllDisplayValue) { - if (bind.peerGetDefaultSessionsCount(id: peerId) > 1) { + if (bind.peerGetSessionsCount( + id: peerId, connType: parent.target!.connType.index) > + 1) { if (display != _pi.currentDisplay) { return; } @@ -809,7 +813,9 @@ class FfiModel with ChangeNotifier { _pi.primaryDisplay = currentDisplay; } - if (bind.peerGetDefaultSessionsCount(id: peerId) <= 1) { + if (bind.peerGetSessionsCount( + id: peerId, connType: parent.target!.connType.index) <= + 1) { _pi.currentDisplay = currentDisplay; } @@ -827,9 +833,11 @@ class FfiModel with ChangeNotifier { sessionId: sessionId, arg: kOptionTouchMode) != ''; } + // FIXME: handle ViewCamera ConnType independently. if (connType == ConnType.fileTransfer) { parent.target?.fileModel.onReady(); - } else if (connType == ConnType.defaultConn) { + } else if (connType == ConnType.defaultConn || + connType == ConnType.viewCamera) { List newDisplays = []; List displays = json.decode(evt['displays']); for (int i = 0; i < displays.length; ++i) { @@ -859,7 +867,7 @@ class FfiModel with ChangeNotifier { bind.sessionGetToggleOptionSync( sessionId: sessionId, arg: kOptionToggleViewOnly)); } - if (connType == ConnType.defaultConn) { + if (connType == ConnType.defaultConn || connType == ConnType.viewCamera) { final platformAdditions = evt['platform_additions']; if (platformAdditions != null && platformAdditions != '') { try { @@ -2576,7 +2584,8 @@ class ElevationModel with ChangeNotifier { onPortableServiceRunning(bool running) => _running = running; } -enum ConnType { defaultConn, fileTransfer, portForward, rdp } +// The index values of `ConnType` are same as rust protobuf. +enum ConnType { defaultConn, fileTransfer, portForward, rdp, viewCamera } /// Flutter state manager and data communication with the Rust core. class FFI { @@ -2651,10 +2660,11 @@ class FFI { ffiModel.waitForImageTimer = null; } - /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. + /// Start with the given [id]. Only transfer file if [isFileTransfer], only view camera if [isViewCamera], only port forward if [isPortForward]. void start( String id, { bool isFileTransfer = false, + bool isViewCamera = false, bool isPortForward = false, bool isRdp = false, String? switchUuid, @@ -2669,9 +2679,15 @@ class FFI { closed = false; auditNote = ''; if (isMobile) mobileReset(); - assert(!(isFileTransfer && isPortForward), 'more than one connect type'); + assert( + (!(isPortForward && isViewCamera)) && + (!(isViewCamera && isPortForward)) && + (!(isPortForward && isFileTransfer)), + 'more than one connect type'); if (isFileTransfer) { connType = ConnType.fileTransfer; + } else if (isViewCamera) { + connType = ConnType.viewCamera; } else if (isPortForward) { connType = ConnType.portForward; } else { @@ -2691,6 +2707,7 @@ class FFI { sessionId: sessionId, id: id, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, isPortForward: isPortForward, isRdp: isRdp, switchUuid: switchUuid ?? '', @@ -2706,7 +2723,10 @@ class FFI { return; } final addRes = bind.sessionAddExistedSync( - id: id, sessionId: sessionId, displays: Int32List.fromList(displays)); + id: id, + sessionId: sessionId, + displays: Int32List.fromList(displays), + isViewCamera: isViewCamera); if (addRes != '') { debugPrint( 'Unreachable, failed to add existed session to $id, $addRes'); @@ -2717,6 +2737,11 @@ class FFI { if (isDesktop && connType == ConnType.defaultConn) { textureModel.updateCurrentDisplay(display ?? 0); } + // FIXME: separate cameras displays or shift all indices. + if (isDesktop && connType == ConnType.viewCamera) { + // FIXME: currently the default 0 is not used. + textureModel.updateCurrentDisplay(display ?? 0); + } // CAUTION: `sessionStart()` and `sessionStartWithDisplays()` are an async functions. // Though the stream is returned immediately, the stream may not be ready. @@ -2993,6 +3018,9 @@ class PeerInfo with ChangeNotifier { bool get isAmyuniIdd => platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd'; + bool get isSupportViewCamera => + platformAdditions[kPlatformAdditionsSupportViewCamera] == true; + Display? tryGetDisplay({int? display}) { if (displays.isEmpty) { return null; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 8775764619e..2ac5bfe16fa 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -791,6 +791,7 @@ class ServerModel with ChangeNotifier { enum ClientType { remote, file, + camera, portForward, } @@ -798,6 +799,7 @@ class Client { int id = 0; // client connections inner count id bool authorized = false; bool isFileTransfer = false; + bool isViewCamera = false; String portForward = ""; String name = ""; String peerId = ""; // peer user's id,show at app @@ -815,13 +817,15 @@ class Client { RxInt unreadChatMessageCount = 0.obs; - Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId, + Client(this.id, this.authorized, this.isFileTransfer, this.isViewCamera, this.name, this.peerId, this.keyboard, this.clipboard, this.audio); Client.fromJson(Map json) { id = json['id']; authorized = json['authorized']; isFileTransfer = json['is_file_transfer']; + // TODO: no entry then default. + isViewCamera = json['is_view_camera']; portForward = json['port_forward']; name = json['name']; peerId = json['peer_id']; @@ -843,6 +847,7 @@ class Client { data['id'] = id; data['authorized'] = authorized; data['is_file_transfer'] = isFileTransfer; + data['is_view_camera'] = isViewCamera; data['port_forward'] = portForward; data['name'] = name; data['peer_id'] = peerId; @@ -863,6 +868,8 @@ class Client { ClientType type_() { if (isFileTransfer) { return ClientType.file; + } else if (isViewCamera) { + return ClientType.camera; } else if (portForward.isNotEmpty) { return ClientType.portForward; } else { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 70001ffdff4..4e848ea7c9e 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -11,7 +11,14 @@ import 'package:flutter_hbb/models/input_model.dart'; /// must keep the order // ignore: constant_identifier_names -enum WindowType { Main, RemoteDesktop, FileTransfer, PortForward, Unknown } +enum WindowType { + Main, + RemoteDesktop, + FileTransfer, + ViewCamera, + PortForward, + Unknown +} extension Index on int { WindowType get windowType { @@ -23,6 +30,8 @@ extension Index on int { case 2: return WindowType.FileTransfer; case 3: + return WindowType.ViewCamera; + case 4: return WindowType.PortForward; default: return WindowType.Unknown; @@ -50,31 +59,46 @@ class RustDeskMultiWindowManager { final List _windowActiveCallbacks = List.empty(growable: true); final List _remoteDesktopWindows = List.empty(growable: true); final List _fileTransferWindows = List.empty(growable: true); + final List _viewCameraWindows = List.empty(growable: true); final List _portForwardWindows = List.empty(growable: true); - moveTabToNewWindow(int windowId, String peerId, String sessionId) async { + moveTabToNewWindow(int windowId, String peerId, String sessionId, + WindowType windowType) async { var params = { - 'type': WindowType.RemoteDesktop.index, + 'type': windowType.index, 'id': peerId, 'tab_window_id': windowId, 'session_id': sessionId, }; - await _newSession( - false, - WindowType.RemoteDesktop, - kWindowEventNewRemoteDesktop, - peerId, - _remoteDesktopWindows, - jsonEncode(params), - ); + if (windowType == WindowType.RemoteDesktop) { + await _newSession( + false, + WindowType.RemoteDesktop, + kWindowEventNewRemoteDesktop, + peerId, + _remoteDesktopWindows, + jsonEncode(params), + ); + } else if (windowType == WindowType.ViewCamera) { + await _newSession( + false, + WindowType.ViewCamera, + kWindowEventNewViewCamera, + peerId, + _viewCameraWindows, + jsonEncode(params), + ); + } } // This function must be called in the main window thread. // Because the _remoteDesktopWindows is managed in that thread. openMonitorSession(int windowId, String peerId, int display, int displayCount, - Rect? screenRect) async { - if (_remoteDesktopWindows.length > 1) { - for (final windowId in _remoteDesktopWindows) { + Rect? screenRect, int windowType) async { + final isCamera = windowType == WindowType.ViewCamera.index; + final windowIDs = isCamera ? _viewCameraWindows : _remoteDesktopWindows; + if (windowIDs.length > 1) { + for (final windowId in windowIDs) { if (await DesktopMultiWindow.invokeMethod( windowId, kWindowEventActiveDisplaySession, @@ -91,7 +115,7 @@ class RustDeskMultiWindowManager { ? List.generate(displayCount, (index) => index) : [display]; var params = { - 'type': WindowType.RemoteDesktop.index, + 'type': windowType, 'id': peerId, 'tab_window_id': windowId, 'display': display, @@ -107,10 +131,10 @@ class RustDeskMultiWindowManager { } await _newSession( false, - WindowType.RemoteDesktop, - kWindowEventNewRemoteDesktop, + windowType.windowType, + isCamera ? kWindowEventNewViewCamera : kWindowEventNewRemoteDesktop, peerId, - _remoteDesktopWindows, + windowIDs, jsonEncode(params), screenRect: screenRect, ); @@ -277,6 +301,27 @@ class RustDeskMultiWindowManager { ); } + Future newViewCamera( + String remoteId, { + String? password, + bool? isSharedPassword, + String? switchUuid, + bool? forceRelay, + String? connToken, + }) async { + return await newSession( + WindowType.ViewCamera, + kWindowEventNewViewCamera, + remoteId, + _viewCameraWindows, + password: password, + forceRelay: forceRelay, + switchUuid: switchUuid, + isSharedPassword: isSharedPassword, + connToken: connToken, + ); + } + Future newPortForward( String remoteId, bool isRDP, { @@ -324,6 +369,8 @@ class RustDeskMultiWindowManager { return _remoteDesktopWindows; case WindowType.FileTransfer: return _fileTransferWindows; + case WindowType.ViewCamera: + return _viewCameraWindows; case WindowType.PortForward: return _portForwardWindows; case WindowType.Unknown: @@ -342,6 +389,9 @@ class RustDeskMultiWindowManager { case WindowType.FileTransfer: _fileTransferWindows.clear(); break; + case WindowType.ViewCamera: + _viewCameraWindows.clear(); + break; case WindowType.PortForward: _portForwardWindows.clear(); break; diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index dba7fc0941c..331012aa90f 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -60,7 +60,8 @@ class RustdeskImpl { throw UnimplementedError("hostStopSystemKeyPropagate"); } - int peerGetDefaultSessionsCount({required String id, dynamic hint}) { + int peerGetSessionsCount( + {required String id, required int connType, dynamic hint}) { return 0; } @@ -68,6 +69,7 @@ class RustdeskImpl { {required String id, required UuidValue sessionId, required Int32List displays, + required bool isViewCamera, dynamic hint}) { return ''; } @@ -76,6 +78,7 @@ class RustdeskImpl { {required UuidValue sessionId, required String id, required bool isFileTransfer, + required bool isViewCamera, required bool isPortForward, required bool isRdp, required String switchUuid, @@ -90,7 +93,8 @@ class RustdeskImpl { 'id': id, 'password': password, 'is_shared_password': isSharedPassword, - 'isFileTransfer': isFileTransfer + 'isFileTransfer': isFileTransfer, + 'isViewCamera': isViewCamera }) ]); } diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart index 21b87b84895..342764b4a63 100644 --- a/flutter/test/cm_test.dart +++ b/flutter/test/cm_test.dart @@ -10,10 +10,10 @@ import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; final testClients = [ - Client(0, false, false, "UserAAAAAA", "123123123", true, false, false), - Client(1, false, false, "UserBBBBB", "221123123", true, false, false), - Client(2, false, false, "UserC", "331123123", true, false, false), - Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false) + Client(0, false, false, false, "UserAAAAAA", "123123123", true, false, false, false), + Client(1, false, false, false, "UserBBBBB", "221123123", true, false, false, false), + Client(2, false, false, false, "UserC", "331123123", true, false, false, false), + Client(3, false, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false, false) ]; /// flutter run -d {platform} -t test/cm_test.dart to test cm diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index 529010f1607..d336f2d3b1e 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -23,6 +23,7 @@ lazy_static = "1.4" hbb_common = { path = "../hbb_common" } webm = { git = "https://github.com/rustdesk-org/rust-webm" } serde = {version="1.0", features=["derive"]} +nokhwa = { git = "https://github.com/rustdesk-org/nokhwa.git", branch = "fix_from_raw_parts", features = ["input-native"] } [dependencies.winapi] version = "0.3" diff --git a/libs/scrap/src/common/camera.rs b/libs/scrap/src/common/camera.rs new file mode 100644 index 00000000000..e857423ed80 --- /dev/null +++ b/libs/scrap/src/common/camera.rs @@ -0,0 +1,232 @@ +use std::{ + io, + sync::{Arc, Mutex}, +}; + +use nokhwa::{ + pixel_format::RgbAFormat, + query, + utils::{ApiBackend, CameraIndex, RequestedFormat, RequestedFormatType}, + Camera, +}; + +use hbb_common::message_proto::{DisplayInfo, Resolution}; + +#[cfg(feature = "vram")] +use crate::AdapterDevice; + +use crate::common::{bail, ResultType}; +use crate::{Frame, PixelBuffer, Pixfmt, TraitCapturer}; + +pub const PRIMARY_CAMERA_IDX: usize = 0; +lazy_static::lazy_static! { + static ref SYNC_CAMERA_DISPLAYS: Arc>> = Arc::new(Mutex::new(Vec::new())); +} + +pub struct Cameras; + +// pre-condition +pub fn primary_camera_exists() -> bool { + Cameras::exists(PRIMARY_CAMERA_IDX) +} + +impl Cameras { + pub fn all_info() -> ResultType> { + // TODO: support more platforms. + #[cfg(not(any(target_os = "linux", target_os = "windows")))] + return Ok(Vec::new()); + + match query(ApiBackend::Auto) { + Ok(cameras) => { + let mut camera_displays = SYNC_CAMERA_DISPLAYS.lock().unwrap(); + camera_displays.clear(); + // FIXME: nokhwa returns duplicate info for one physical camera on linux for now. + // issue: https://github.com/l1npengtul/nokhwa/issues/171 + // Use only one camera as a temporary hack. + cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + let Some(info) = cameras.first() else { + bail!("No camera found") + }; + let camera = Self::create_camera(info.index())?; + let resolution = camera.resolution(); + let (width, height) = (resolution.width() as i32, resolution.height() as i32); + camera_displays.push(DisplayInfo { + x: 0, + y: 0, + name: info.human_name().clone(), + width, + height, + online: true, + cursor_embedded: false, + scale:1.0, + original_resolution: Some(Resolution { + width, + height, + ..Default::default() + }).into(), + ..Default::default() + }); + } else { + let mut x = 0; + for info in &cameras { + let camera = Self::create_camera(info.index())?; + let resolution = camera.resolution(); + let (width, height) = (resolution.width() as i32, resolution.height() as i32); + camera_displays.push(DisplayInfo { + x, + y: 0, + name: info.human_name().clone(), + width, + height, + online: true, + cursor_embedded: false, + scale:1.0, + original_resolution: Some(Resolution { + width, + height, + ..Default::default() + }).into(), + ..Default::default() + }); + x += width; + } + } + } + Ok(camera_displays.clone()) + } + Err(e) => { + bail!("Query cameras error: {}", e) + } + } + } + + pub fn exists(index: usize) -> bool { + // TODO: support more platforms. + #[cfg(not(any(target_os = "linux", target_os = "windows")))] + return false; + + match query(ApiBackend::Auto) { + Ok(cameras) => index < cameras.len(), + _ => return false, + } + } + + fn create_camera(index: &CameraIndex) -> ResultType { + // TODO: support more platforms. + #[cfg(not(any(target_os = "linux", target_os = "windows")))] + bail!("This platform doesn't support camera yet"); + + let result = Camera::new( + index.clone(), + RequestedFormat::new::(RequestedFormatType::AbsoluteHighestResolution), + ); + match result { + Ok(camera) => Ok(camera), + Err(e) => bail!("create camera{} error: {}", index, e), + } + } + + pub fn get_camera_resolution(index: usize) -> ResultType { + let index = CameraIndex::Index(index as u32); + let camera = Self::create_camera(&index)?; + let resolution = camera.resolution(); + Ok(Resolution { + width: resolution.width() as i32, + height: resolution.height() as i32, + ..Default::default() + }) + } + + pub fn get_sync_cameras() -> Vec { + SYNC_CAMERA_DISPLAYS.lock().unwrap().clone() + } + + pub fn get_capturer(current: usize) -> ResultType> { + Ok(Box::new(CameraCapturer::new(current)?)) + } +} + +pub struct CameraCapturer { + camera: Camera, + data: Vec, + last_data: Vec, // for faster compare and copy +} + +impl CameraCapturer { + fn new(current: usize) -> ResultType { + let index = CameraIndex::Index(current as u32); + let camera = Cameras::create_camera(&index)?; + Ok(CameraCapturer { + camera, + data: Vec::new(), + last_data: Vec::new(), + }) + } +} + +impl TraitCapturer for CameraCapturer { + fn frame<'a>(&'a mut self, _timeout: std::time::Duration) -> std::io::Result> { + // TODO: move this check outside `frame`. + if !self.camera.is_stream_open() { + if let Err(e) = self.camera.open_stream() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera open stream error: {}", e), + )); + } + } + match self.camera.frame() { + Ok(buffer) => { + match buffer.decode_image::() { + Ok(decoded) => { + self.data = decoded.as_raw().to_vec(); + crate::would_block_if_equal(&mut self.last_data, &self.data)?; + // FIXME: macos's PixelBuffer cannot be directly created from bytes slice. + cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "windows"))] { + Ok(Frame::PixelBuffer(PixelBuffer::new( + &self.data, + Pixfmt::RGBA, + decoded.width() as usize, + decoded.height() as usize, + ))) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera is not supported on this platform yet"), + )) + } + } + } + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera frame decode error: {}", e), + )), + } + } + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera frame error: {}", e), + )), + } + } + + #[cfg(windows)] + fn is_gdi(&self) -> bool { + false + } + + #[cfg(windows)] + fn set_gdi(&mut self) -> bool { + false + } + + #[cfg(feature = "vram")] + fn device(&self) -> AdapterDevice { + AdapterDevice::default() + } + + #[cfg(feature = "vram")] + fn set_output_texture(&mut self, _texture: bool) {} +} diff --git a/libs/scrap/src/common/dxgi.rs b/libs/scrap/src/common/dxgi.rs index ae2f1130feb..f7bf167d2d2 100644 --- a/libs/scrap/src/common/dxgi.rs +++ b/libs/scrap/src/common/dxgi.rs @@ -70,23 +70,30 @@ impl TraitCapturer for Capturer { pub struct PixelBuffer<'a> { data: &'a [u8], + pixfmt: Pixfmt, width: usize, height: usize, stride: Vec, } impl<'a> PixelBuffer<'a> { - pub fn new(data: &'a [u8], width: usize, height: usize) -> Self { + pub fn new(data: &'a [u8], pixfmt: Pixfmt, width: usize, height: usize) -> Self { let stride0 = data.len() / height; let mut stride = Vec::new(); stride.push(stride0); PixelBuffer { data, + pixfmt, width, height, stride, } } + + #[allow(non_snake_case)] + pub fn with_BGRA(data: &'a [u8], width: usize, height: usize) -> Self { + Self::new(data, Pixfmt::BGRA, width, height) + } } impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> { @@ -107,7 +114,7 @@ impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> { } fn pixfmt(&self) -> Pixfmt { - Pixfmt::BGRA + self.pixfmt } } @@ -232,7 +239,7 @@ impl CapturerMag { impl TraitCapturer for CapturerMag { fn frame<'a>(&'a mut self, _timeout_ms: Duration) -> io::Result> { self.inner.frame(&mut self.data)?; - Ok(Frame::PixelBuffer(PixelBuffer::new( + Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( &self.data, self.inner.get_rect().1, self.inner.get_rect().2, diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index cef718cc109..7b8388fbd5b 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -48,6 +48,7 @@ pub use self::convert::*; pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer +pub mod camera; pub mod aom; pub mod record; mod vpx; diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index 6a1a6d60fcb..628d2d5a3f7 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -25,7 +25,7 @@ pub struct RecorderContext { pub server: bool, pub id: String, pub dir: String, - pub display: usize, + pub video_service_name: String, pub tx: Option>, } @@ -46,7 +46,7 @@ impl RecorderContext2 { + "_" + &ctx.id.clone() + &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string() - + &format!("display{}_", ctx.display) + + &format!("{}_", ctx.video_service_name) + &self.format.to_string().to_lowercase() + if self.format == CodecFormat::VP9 || self.format == CodecFormat::VP8 diff --git a/libs/scrap/src/common/vram.rs b/libs/scrap/src/common/vram.rs index 747dbbe76db..c003fa698e8 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}, + codec::{enable_vram_option, EncoderApi, EncoderCfg}, hwcodec::HwCodecConfig, AdapterDevice, CodecFormat, EncodeInput, EncodeYuvFormat, Pixfmt, }; @@ -30,8 +30,8 @@ use hwcodec::{ // https://cybersided.com/two-monitors-two-gpus/ // https://learn.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-getadapterluid#remarks lazy_static::lazy_static! { - static ref ENOCDE_NOT_USE: Arc>> = Default::default(); - static ref FALLBACK_GDI_DISPLAYS: Arc>> = Default::default(); + static ref ENOCDE_NOT_USE: Arc>> = Default::default(); + static ref FALLBACK_GDI_DISPLAYS: Arc>> = Default::default(); } #[derive(Debug, Clone)] @@ -287,16 +287,25 @@ impl VRamEncoder { crate::hwcodec::HwRamEncoder::calc_bitrate(width, height, ratio, fmt == DataFormat::H264) } - pub fn set_not_use(display: usize, not_use: bool) { - log::info!("set display#{display} not use vram encode to {not_use}"); - ENOCDE_NOT_USE.lock().unwrap().insert(display, not_use); + pub fn set_not_use(video_service_name: String, not_use: bool) { + log::info!("set {video_service_name} not use vram encode to {not_use}"); + ENOCDE_NOT_USE + .lock() + .unwrap() + .insert(video_service_name, not_use); } - pub fn set_fallback_gdi(display: usize, fallback: bool) { + pub fn set_fallback_gdi(video_service_name: String, fallback: bool) { if fallback { - FALLBACK_GDI_DISPLAYS.lock().unwrap().insert(display); + FALLBACK_GDI_DISPLAYS + .lock() + .unwrap() + .insert(video_service_name); } else { - FALLBACK_GDI_DISPLAYS.lock().unwrap().remove(&display); + FALLBACK_GDI_DISPLAYS + .lock() + .unwrap() + .remove(&video_service_name); } } } diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index a9d1af85c0e..1f5296954d0 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -396,7 +396,7 @@ impl Capturer { } else { let width = self.width; let height = self.height; - Ok(Frame::PixelBuffer(PixelBuffer::new( + Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( self.get_pixelbuffer(timeout)?, width, height, diff --git a/src/client.rs b/src/client.rs index 488e388eb6b..380559bb803 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1389,14 +1389,14 @@ impl VideoHandler { } /// Start or stop screen record. - pub fn record_screen(&mut self, start: bool, id: String, display: usize) { + pub fn record_screen(&mut self, start: bool, id: String, video_service_name: String) { self.record = false; if start { self.recorder = Recorder::new(RecorderContext { server: false, id, dir: crate::ui_interface::video_save_directory(false), - display, + video_service_name, tx: None, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); @@ -2349,6 +2349,7 @@ impl LoginConfigHandler { show_hidden: !self.get_option("remote_show_hidden").is_empty(), ..Default::default() }), + ConnType::VIEW_CAMERA => lr.set_view_camera(Default::default()), ConnType::PORT_FORWARD | ConnType::RDP => lr.set_port_forward(PortForward { host: self.port_forward.0.clone(), port: self.port_forward.1, @@ -2436,6 +2437,14 @@ pub fn start_video_thread( { let mut video_callback = video_callback; let mut last_chroma = None; + let video_service_name = crate::video_service::get_service_name( + if session.is_view_camera() { + crate::video_service::VideoSource::Camera + } else { + crate::video_service::VideoSource::Monitor + }, + display, + ); std::thread::spawn(move || { #[cfg(windows)] @@ -2478,7 +2487,7 @@ pub fn start_video_thread( let record_permission = session.lc.read().unwrap().record_permission; let id = session.lc.read().unwrap().id.clone(); if record_state && record_permission { - handler.record_screen(true, id, display); + handler.record_screen(true, id, video_service_name.clone()); } video_handler = Some(handler); } @@ -2559,7 +2568,7 @@ pub fn start_video_thread( MediaData::RecordScreen(start) => { let id = session.lc.read().unwrap().id.clone(); if let Some(handler) = video_handler.as_mut() { - handler.record_screen(start, id, display); + handler.record_screen(start, id, video_service_name.clone()); } } _ => {} diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 448eac7f966..4f084151f78 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -83,6 +83,7 @@ struct ParsedPeerInfo { platform: String, is_installed: bool, idd_impl: String, + support_view_camera: bool, } impl ParsedPeerInfo { @@ -129,7 +130,10 @@ impl Remote { #[cfg(target_os = "windows")] 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() { + if !self.handler.is_file_transfer() + && !self.handler.is_port_forward() + && !self.handler.is_view_camera() + { // It is ok to call this function multiple times. ContextSend::enable(true); Some(crate::SimpleCallOnReturn { @@ -152,6 +156,8 @@ impl Remote { let mut received = false; let conn_type = if self.handler.is_file_transfer() { ConnType::FILE_TRANSFER + } else if self.handler.is_view_camera() { + ConnType::VIEW_CAMERA } else { ConnType::default() }; @@ -173,7 +179,7 @@ impl Remote { .set_connected(); self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready self.handler.update_direct(Some(direct)); - if conn_type == ConnType::DEFAULT_CONN { + if conn_type == ConnType::DEFAULT_CONN || conn_type == ConnType::VIEW_CAMERA { self.handler .set_fingerprint(crate::common::pk_to_fingerprint(pk.unwrap_or_default())); } @@ -190,7 +196,8 @@ impl Remote { { let is_conn_not_default = self.handler.is_file_transfer() || self.handler.is_port_forward() - || self.handler.is_rdp(); + || self.handler.is_rdp() + || self.handler.is_view_camera(); if !is_conn_not_default { (self.client_conn_id, rx_clip_client_holder.0) = clipboard::get_rx_cliprdr_client(&self.handler.get_id()); @@ -330,12 +337,12 @@ impl Remote { .set_disconnected(round); #[cfg(not(target_os = "ios"))] - if _set_disconnected_ok { + if !self.handler.is_view_camera() && _set_disconnected_ok { Client::try_stop_clipboard(); } #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] - if _set_disconnected_ok { + if !self.handler.is_view_camera() && _set_disconnected_ok { crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id); } } @@ -1176,6 +1183,25 @@ impl Remote { } } + fn check_view_camera_support(&self, peer_version: &str, peer_platform: &str) -> bool { + if self.peer_info.support_view_camera { + return true; + } + if hbb_common::get_version_number(&peer_version) < hbb_common::get_version_number("1.3.9") + && (peer_platform == "Windows" || peer_platform == "Linux") + { + self.handler.msgbox( + "error", + "Download new version", + "upgrade_remote_rustdesk_client_to_{1.3.9}_tip", + "", + ); + } else { + self.handler.on_error("view_camera_unsupported_tip"); + } + return false; + } + async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { if let Ok(msg_in) = Message::parse_from_bytes(&data) { match msg_in.union { @@ -1230,10 +1256,19 @@ impl Remote { let peer_version = pi.version.clone(); let peer_platform = pi.platform.clone(); self.set_peer_info(&pi); + if self.handler.is_view_camera() { + if !self.check_view_camera_support(&peer_version, &peer_platform) { + self.handler.lc.write().unwrap().handle_peer_info(&pi); + return false; + } + } self.handler.handle_peer_info(pi); #[cfg(all(target_os = "windows", not(feature = "flutter")))] self.check_clipboard_file_context(); - if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { + if !(self.handler.is_file_transfer() + || self.handler.is_port_forward() + || self.handler.is_view_camera()) + { #[cfg(feature = "flutter")] #[cfg(not(target_os = "ios"))] let rx = Client::try_start_clipboard(None); @@ -1532,6 +1567,9 @@ impl Remote { ); } } + Ok(Permission::Camera) => { + self.handler.set_permission("camera", p.enabled); + } Ok(Permission::Restart) => { self.handler.set_permission("restart", p.enabled); } @@ -1773,6 +1811,11 @@ impl Remote { .flatten() .unwrap_or_default() .to_string(); + self.peer_info.support_view_camera = platform_additions + .get("support_view_camera") + .map(|v| v.as_bool()) + .flatten() + .unwrap_or(false); } } diff --git a/src/core_main.rs b/src/core_main.rs index 182a04a1629..4e1fc115955 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -53,6 +53,7 @@ pub fn core_main() -> Option> { "--connect", "--play", "--file-transfer", + "--view-camera", "--port-forward", "--rdp", ] @@ -99,7 +100,7 @@ pub fn core_main() -> Option> { } } #[cfg(windows)] - if args.contains(&"--connect".to_string()) { + if args.contains(&"--connect".to_string()) || args.contains(&"--view-camera".to_string()) { hbb_common::platform::windows::start_cpu_performance_monitor(); } #[cfg(feature = "flutter")] @@ -589,7 +590,7 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option { + "--connect" | "--play" | "--file-transfer" | "--view-camera" | "--port-forward" | "--rdp" => { authority = Some((&arg.to_string()[2..]).to_owned()); id = args.next(); } diff --git a/src/flutter.rs b/src/flutter.rs index 2bc8066edad..2e86f6893f3 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1149,8 +1149,14 @@ pub fn session_add_existed( peer_id: String, session_id: SessionID, displays: Vec, + is_view_camera: bool, ) -> ResultType<()> { - sessions::insert_peer_session_id(peer_id, ConnType::DEFAULT_CONN, session_id, displays); + let conn_type = if is_view_camera { + ConnType::VIEW_CAMERA + } else { + ConnType::DEFAULT_CONN + }; + sessions::insert_peer_session_id(peer_id, conn_type, session_id, displays); Ok(()) } @@ -1160,11 +1166,13 @@ pub fn session_add_existed( /// /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ /// * `is_file_transfer` - If the session is used for file transfer. +/// * `is_view_camera` - If the session is used for view camera. /// * `is_port_forward` - If the session is used for port forward. pub fn session_add( session_id: &SessionID, id: &str, is_file_transfer: bool, + is_view_camera: bool, is_port_forward: bool, is_rdp: bool, switch_uuid: &str, @@ -1175,6 +1183,8 @@ pub fn session_add( ) -> ResultType { let conn_type = if is_file_transfer { ConnType::FILE_TRANSFER + } else if is_view_camera { + ConnType::VIEW_CAMERA } else if is_port_forward { if is_rdp { ConnType::RDP diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index cc9d304e7c5..c36b5844dad 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -92,16 +92,28 @@ pub fn host_stop_system_key_propagate(_stopped: bool) { } // This function is only used to count the number of control sessions. -pub fn peer_get_default_sessions_count(id: String) -> SyncReturn { - SyncReturn(sessions::get_session_count(id, ConnType::DEFAULT_CONN)) +pub fn peer_get_sessions_count(id: String, conn_type: i32) -> SyncReturn { + let conn_type = if conn_type == ConnType::VIEW_CAMERA as i32 { + ConnType::VIEW_CAMERA + } else if conn_type == ConnType::FILE_TRANSFER as i32 { + ConnType::FILE_TRANSFER + } else if conn_type == ConnType::PORT_FORWARD as i32 { + ConnType::PORT_FORWARD + } else if conn_type == ConnType::RDP as i32 { + ConnType::RDP + } else { + ConnType::DEFAULT_CONN + }; + SyncReturn(sessions::get_session_count(id, conn_type)) } pub fn session_add_existed_sync( id: String, session_id: SessionID, displays: Vec, + is_view_camera: bool, ) -> SyncReturn { - if let Err(e) = session_add_existed(id.clone(), session_id, displays) { + if let Err(e) = session_add_existed(id.clone(), session_id, displays, is_view_camera) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) @@ -112,6 +124,7 @@ pub fn session_add_sync( session_id: SessionID, id: String, is_file_transfer: bool, + is_view_camera: bool, is_port_forward: bool, is_rdp: bool, switch_uuid: String, @@ -124,6 +137,7 @@ pub fn session_add_sync( &session_id, &id, is_file_transfer, + is_view_camera, is_port_forward, is_rdp, &switch_uuid, diff --git a/src/ipc.rs b/src/ipc.rs index 5f533cd94ab..7e447576b0f 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -188,6 +188,7 @@ pub enum Data { Login { id: i32, is_file_transfer: bool, + is_view_camera: bool, peer_id: String, name: String, authorized: bool, @@ -1280,6 +1281,6 @@ mod test { #[test] fn verify_ffi_enum_data_size() { println!("{}", std::mem::size_of::()); - assert!(std::mem::size_of::() < 96); + assert!(std::mem::size_of::() <= 96); } } diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 8e7f90f1fad..c96870b1553 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "عرض الكاميرا"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index c9b5a6b4932..039bc2e2519 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Прагляд камеры"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Калі ласка, абнавіце кліент RustDesk да версіі {} або навейшай на аддаленым баку!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index af5e4c428a4..3b6e3171ec3 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Преглед на камерата"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Моля, надстройте клиента RustDesk до версия {} или по-нова от отдалечената страна!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e5b3ea1592b..746b32e71c3 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sense etiquetar"), ("new-version-of-{}-tip", ""), ("Accessible devices", "Dispositius accessibles"), + ("View camera", "Mostra la càmera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre à niveau le client RustDesk vers la version {} ou plus récente du côté distant !"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 7ede944cbad..bef8122a600 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "无标签"), ("new-version-of-{}-tip", "{} 版本更新"), ("Accessible devices", "可访问的设备"), + ("View camera", "查看摄像头"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "请在远程端将 RustDesk 客户端升级至版本 {} 或更新版本!"), + ("view_camera_unsupported_tip", "您的远程端不支持查看摄像头。"), + ("Enable camera", "允许查看摄像头"), + ("No cameras", "没有摄像头"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 4b070eb2db6..941a8f035a4 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Zobrazit kameru"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgradujte prosím klienta RustDesk na verzi {} nebo novější na vzdálené straně!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 1d22ea9a087..177c040ace9 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Se kamera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Opgrader venligst RustDesk-klienten til version {} eller nyere på fjernsiden!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index e482085d851..2589a826e29 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Unmarkiert"), ("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"), ("Accessible devices", "Erreichbare Geräte"), + ("View camera", "Kamera anzeigen"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Bitte aktualisieren Sie den RustDesk-Client auf der Remote-Seite auf Version {} oder neuer!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 5f8dae0b129..13c0889c31e 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Χωρίς ετικέτα"), ("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"), ("Accessible devices", "Προσβάσιμες συσκευές"), + ("View camera", "Προβολή κάμερας"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότερη στην απομακρυσμένη πλευρά!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 5fe3016ca07..3b20a5bf298 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -237,5 +237,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), ("new-version-of-{}-tip", "There is a new version of {} available"), + ("View camera", "View camera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Please upgrade the RustDesk client to version {} or newer on the remote side!"), + ("view_camera_unsupported_tip", "The remote device does not support viewing the camera."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 4bcafc03f00..903dad827f3 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Rigardi kameron"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 245e4415758..0c56f95ccfc 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sin itiquetar"), ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), ("Accessible devices", ""), + ("View camera", "Ver cámara"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index e40a2a477f2..c0ccef343d3 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -600,7 +600,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Everyone", "Igaüks"), ("ab_web_console_tip", "Rohkem leiad veebikonsoolist"), ("allow-only-conn-window-open-tip", "Luba ühendus ainult siis, kui RustDeski aken on avatud."), - ("no_need_privacy_mode_no_physical_displays_tip", "Füüsilisi ekraane pole, privaatsusrežiimi kasutamine pole vajalik."), + ("no_need_privacy_mode_no_physical_displays_tip", "Füüsilisi ekraane pole, privaatsusrežiimi kasutamine pole vajalik."), ("Follow remote cursor", "Jälgi kaugkursorit"), ("Follow remote window focus", "Jälgi kaugakna fookust"), ("default_proxy_tip", "Vaikimisi protokoll ja port on Socks5 ja 1080."), @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sildistamata"), ("new-version-of-{}-tip", "Saadaval on {} uus versioon"), ("Accessible devices", "Ligipääsetavad seadmed"), + ("View camera", "Vaata kaamerat"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Täiendage RustDeski klient kaugküljel versioonile {} või uuemale!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 9b7bc9a8ead..54939966488 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Ikusi kamera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Mesedez, eguneratu RustDesk bezeroa {} bertsiora edo berriagoa urruneko aldean!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 816fc6c3e8e..5db84e9e6c6 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "نمایش دوربین"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "لطفاً مشتری RustDesk را به نسخه {} یا جدیدتر در سمت راه دور ارتقا دهید!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 23c482fc608..456dd1d5ca3 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sans étiquette"), ("new-version-of-{}-tip", "Une nouvelle version de {} est disponible"), ("Accessible devices", "Appareils accessibles"), + ("View camera", "Voir la caméra"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre à jour le client RustDesk avec la version {} ou une version plus récente sur l'appareil distant"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 705d3bb1309..acbeae98023 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "אנא שדרג את לקוח RustDesk לגרסה {} או חדשה יותר בצד המרוחק!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 501da1c12a9..b52dbb4dba3 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Pregled kamere"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Molimo ažurirajte RustDesk klijent na verziju {} ili noviju na udaljenoj strani!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 11c9df22be6..02e53c3a642 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Címkézetlen"), ("new-version-of-{}-tip", "A(z) {} új verziója"), ("Accessible devices", "Hozzáférhető eszközök"), + ("View camera", "Kamera megtekintése"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Kérjük, frissítse a RustDesk kliens {} vagy újabb verziójára a távoli oldalon!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 2666abad769..8dc82ff25e8 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Lihat Kamera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Silakan perbarui klien RustDesk ke versi {} atau lebih baru di sisi remote!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 43f72fd9296..80dbee3ca0c 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Senza tag"), ("new-version-of-{}-tip", "È disponibile una nuova versione di {}"), ("Accessible devices", "Dispositivi accessibili"), + ("View camera", "Visualizza telecamera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Aggiorna il client RustDesk remoto alla versione {} o successiva!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 78cc748fe88..5daa909c550 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "カメラを表示"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "リモート側のRustDeskクライアントをバージョン{}以上にアップグレードしてください!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 8a004809c18..2929fa28cb2 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "태그 없음"), ("new-version-of-{}-tip", "{} 의 새로운 버전이 출시되었습니다."), ("Accessible devices", ""), + ("View camera", "카메라 보기"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "원격 측의 RustDesk 클라이언트를 {} 버전 이상으로 업그레이드하십시오!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index c0a07b9d557..ff5d756c494 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Камераны Көру"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Қашықтағы жақтағы RustDesk клиентін {} немесе одан жоғары нұсқаға жаңартуды өтінеміз!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index e19badc6165..9929508c96e 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Peržiūrėti kamerą"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Prašome atnaujinti nuotolinės pusės RustDesk klientą į {} ar naujesnę versiją!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index b15eafa7602..fe3dce7e82d 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Neatzīmēts"), ("new-version-of-{}-tip", "Ir pieejama jauna {} versija"), ("Accessible devices", "Pieejamas ierīces"), + ("View camera", "Skatīt kameru"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Lūdzu, jauniniet attālās puses RustDesk klientu uz versiju {} vai jaunāku!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 51855906b88..9fb020f4f72 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Vis kamera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 529f47257ed..68b126c5c24 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Ongemarkeerd"), ("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"), ("Accessible devices", "Toegankelijke apparaten"), + ("View camera", "Camera bekijken"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgrade de RustDesk client naar versie {} of nieuwer op de externe computer!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 3d4dd3ac797..e5459081831 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Bez etykiety"), ("new-version-of-{}-tip", "Dostępna jest nowa wersja {}"), ("Accessible devices", "Dostępne urządzenia"), + ("View camera", "Podgląd kamery"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Proszę zaktualizować zdalny klient RustDesk do wersji {} lub nowszej!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e1cde5c3cd4..e5ef2ca0f81 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Ver câmara"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 3a64e4fb091..71c8a480397 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Visualizar Câmera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualize o cliente RustDesk para a versão {} ou superior no lado remoto."), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index b2d02ad7119..ae5283e5b4b 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Vezi camera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index e0c8eaeb147..b79dffe563d 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Без метки"), ("new-version-of-{}-tip", "Доступна новая версия {}"), ("Accessible devices", "Доступные устройства"), + ("View camera", "Просмотр камеры"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Обновите клиент RustDesk до версии {} или новее на удаленной стороне!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index bb49857aff1..4e5e8c76f0b 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Chene tag"), ("new-version-of-{}-tip", "B'at una versione noa de {} a disponimentu"), ("Accessible devices", "Dispositivos atzessìbiles"), + ("View camera", "Mustra càmera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "¡Actualice el cliente RustDesk a la versión {} o más reciente en el lado remoto!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index adbd91fcd9e..eb8f53d81cf 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Zobraziť kameru"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Aktualizujte klienta RustDesk na verziu {} alebo novšiu na vzdialenej strane!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index ada51f77c98..e4061caed68 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Neoznačeno"), ("new-version-of-{}-tip", "Na voljo je nova različica {}"), ("Accessible devices", ""), + ("View camera", "Pogled kamere"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Prosimo, nadgradite RustDesk odjemalec na različico {} ali novejšo na oddaljeni strani."), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index fd35d84ba61..5649130fbd1 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 47d88fadb70..f4ba4048fea 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Pregled kamere"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index f0a20bd5e87..3809373aa21 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Visa kamera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b5edd1f83d8..cc9197b5d47 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index d56fda6d2a3..7674d059da6 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "ดูกล้อง"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "กรุณาอัปเดต RustDesk ไคลเอนต์ไปยังเวอร์ชัน {} หรือใหม่กว่าที่ฝั่งปลายทาง!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 4b4bb484f9e..f7ef7d3279f 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Kamerayı görüntüle"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0eb0b543ba9..43b0e724fab 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "無標籤"), ("new-version-of-{}-tip", "有新版本的 {} 可用"), ("Accessible devices", "可存取的裝置"), + ("View camera", "檢視相機"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "請將遠端 RustDesk 用戶端升級到 {} 或更新版本!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index dfe469518fa..f1f7e76f49e 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Без міток"), ("new-version-of-{}-tip", "Доступна нова версія {}"), ("Accessible devices", ""), + ("View camera", "Перегляд камери"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Будь ласка, оновіть RustDesk клієнт на віддаленому пристрої до версії {} чи новіше!"), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index cd7e6b7dd10..d1254960d65 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -657,5 +657,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", "Xem camera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), ].iter().cloned().collect(); } diff --git a/src/server.rs b/src/server.rs index 117b700c17b..dcb7023f5c4 100644 --- a/src/server.rs +++ b/src/server.rs @@ -24,9 +24,11 @@ use hbb_common::{ sodiumoxide::crypto::{box_, sign}, timeout, tokio, ResultType, Stream, }; +use scrap::camera; #[cfg(not(any(target_os = "android", target_os = "ios")))] use service::ServiceTmpl; use service::{EmptyExtraFieldService, GenericService, Service, Subscriber}; +use video_service::VideoSource; use crate::ipc::Data; @@ -76,7 +78,6 @@ const CONFIG_SYNC_INTERVAL_SECS: f32 = 0.3; lazy_static::lazy_static! { pub static ref CHILD_PROCESS: Childs = Default::default(); - pub static ref CONN_COUNT: Arc> = Default::default(); // A client server used to provide local services(audio, video, clipboard, etc.) // for all initiative connections. // @@ -279,22 +280,53 @@ async fn create_relay_connection_( impl Server { fn is_video_service_name(name: &str) -> bool { - name.starts_with(video_service::NAME) + name.starts_with(VideoSource::Monitor.service_name_prefix()) + || name.starts_with(VideoSource::Camera.service_name_prefix()) + } + + pub fn try_add_primary_camera_service(&mut self) { + if !camera::primary_camera_exists() { + return; + } + let primary_camera_name = + video_service::get_service_name(VideoSource::Camera, camera::PRIMARY_CAMERA_IDX); + if !self.contains(&primary_camera_name) { + self.add_service(Box::new(video_service::new( + VideoSource::Camera, + camera::PRIMARY_CAMERA_IDX, + ))); + } } pub fn try_add_primay_video_service(&mut self) { - let primary_video_service_name = - video_service::get_service_name(*display_service::PRIMARY_DISPLAY_IDX); + let primary_video_service_name = video_service::get_service_name( + VideoSource::Monitor, + *display_service::PRIMARY_DISPLAY_IDX, + ); if !self.contains(&primary_video_service_name) { self.add_service(Box::new(video_service::new( + VideoSource::Monitor, *display_service::PRIMARY_DISPLAY_IDX, ))); } } + pub fn add_camera_connection(&mut self, conn: ConnInner) { + if camera::primary_camera_exists() { + let primary_camera_name = + video_service::get_service_name(VideoSource::Camera, camera::PRIMARY_CAMERA_IDX); + if let Some(s) = self.services.get(&primary_camera_name) { + s.on_subscribe(conn.clone()); + } + } + self.connections.insert(conn.id(), conn); + } + pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) { - let primary_video_service_name = - video_service::get_service_name(*display_service::PRIMARY_DISPLAY_IDX); + let primary_video_service_name = video_service::get_service_name( + VideoSource::Monitor, + *display_service::PRIMARY_DISPLAY_IDX, + ); for s in self.services.values() { let name = s.name(); if Self::is_video_service_name(&name) && name != primary_video_service_name { @@ -307,7 +339,6 @@ impl Server { #[cfg(target_os = "macos")] self.update_enable_retina(); self.connections.insert(conn.id(), conn); - *CONN_COUNT.lock().unwrap() = self.connections.len(); } pub fn remove_connection(&mut self, conn: &ConnInner) { @@ -315,7 +346,6 @@ impl Server { s.on_unsubscribe(conn.id()); } self.connections.remove(&conn.id()); - *CONN_COUNT.lock().unwrap() = self.connections.len(); #[cfg(target_os = "macos")] self.update_enable_retina(); } @@ -361,10 +391,15 @@ impl Server { self.id_count } - pub fn set_video_service_opt(&self, display: Option, opt: &str, value: &str) { + pub fn set_video_service_opt( + &self, + display: Option<(VideoSource, usize)>, + opt: &str, + value: &str, + ) { for (k, v) in self.services.iter() { - if let Some(display) = display { - if k != &video_service::get_service_name(display) { + if let Some((source, display)) = display { + if k != &video_service::get_service_name(source, display) { continue; } } @@ -392,13 +427,14 @@ impl Server { fn capture_displays( &mut self, conn: ConnInner, + source: VideoSource, displays: &[usize], include: bool, exclude: bool, ) { let displays = displays .iter() - .map(|d| video_service::get_service_name(*d)) + .map(|d| video_service::get_service_name(source, *d)) .collect::>(); let keys = self.services.keys().cloned().collect::>(); for name in keys.iter() { diff --git a/src/server/connection.rs b/src/server/connection.rs index 7b1173fd70e..95f4b88effb 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -44,6 +44,7 @@ use hbb_common::{ }; #[cfg(any(target_os = "android", target_os = "ios"))] use scrap::android::{call_main_service_key_event, call_main_service_pointer_input}; +use scrap::camera; use serde_derive::Serialize; use serde_json::{json, value::Value}; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -167,6 +168,7 @@ pub enum AuthConnType { Remote, FileTransfer, PortForward, + ViewCamera, } pub struct Connection { @@ -179,6 +181,7 @@ pub struct Connection { timer: crate::RustDeskInterval, file_timer: crate::RustDeskInterval, file_transfer: Option<(String, bool)>, + view_camera: bool, port_forward_socket: Option>, port_forward_address: String, tx_to_cm: mpsc::UnboundedSender, @@ -222,6 +225,7 @@ pub struct Connection { portable: PortableState, from_switch: bool, voice_call_request_timestamp: Option, + voice_calling: bool, options_in_login: Option, #[cfg(not(any(target_os = "ios")))] pressed_modifiers: HashSet, @@ -331,6 +335,7 @@ impl Connection { timer: crate::rustdesk_interval(time::interval(SEC30)), file_timer: crate::rustdesk_interval(time::interval(SEC30)), file_transfer: None, + view_camera: false, port_forward_socket: None, port_forward_address: "".to_owned(), tx_to_cm, @@ -369,6 +374,7 @@ impl Connection { from_switch: false, audio_sender: None, voice_call_request_timestamp: None, + voice_calling: false, options_in_login: None, #[cfg(not(any(target_os = "ios")))] pressed_modifiers: Default::default(), @@ -533,9 +539,17 @@ impl Connection { conn.send_permission(Permission::Audio, enabled).await; if conn.authorized { if let Some(s) = conn.server.upgrade() { - s.write().unwrap().subscribe( - super::audio_service::NAME, - conn.inner.clone(), conn.audio_enabled()); + if conn.is_authed_view_camera_conn() { + if conn.voice_calling || !conn.audio_enabled() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + conn.inner.clone(), conn.audio_enabled()); + } + } else { + s.write().unwrap().subscribe( + super::audio_service::NAME, + conn.inner.clone(), conn.audio_enabled()); + } } } } else if &name == "file" { @@ -774,7 +788,7 @@ impl Connection { }); conn.send(msg_out.into()).await; } - if conn.is_authed_remote_conn() { + if conn.is_authed_remote_conn() || conn.view_camera { 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()); } @@ -1189,6 +1203,8 @@ impl Connection { (1, AuthConnType::FileTransfer) } else if self.port_forward_socket.is_some() { (2, AuthConnType::PortForward) + } else if self.view_camera { + (3, AuthConnType::ViewCamera) } else { (0, AuthConnType::Remote) }; @@ -1277,6 +1293,11 @@ impl Connection { platform_additions.insert("has_file_clipboard".into(), json!(has_file_clipboard)); } + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + platform_additions.insert("support_view_camera".into(), json!(true)); + } + #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] if !platform_additions.is_empty() { pi.platform_additions = serde_json::to_string(&platform_additions).unwrap_or("".into()); @@ -1290,7 +1311,8 @@ impl Connection { return; } #[cfg(target_os = "linux")] - if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() { + if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() && !self.view_camera + { let mut msg = "".to_string(); if crate::platform::linux::is_login_screen_wayland() { msg = crate::client::LOGIN_SCREEN_WAYLAND.to_owned() @@ -1347,6 +1369,29 @@ impl Connection { self.handle_windows_specific_session(&mut pi, &mut wait_session_id_confirm); if self.file_transfer.is_some() { res.set_peer_info(pi); + } else if self.view_camera { + let supported_encoding = scrap::codec::Encoder::supported_encoding(); + self.last_supported_encoding = Some(supported_encoding.clone()); + log::info!("peer info supported_encoding: {:?}", supported_encoding); + pi.encoding = Some(supported_encoding).into(); + + pi.displays = camera::Cameras::all_info().unwrap_or(Vec::new()); + pi.current_display = camera::PRIMARY_CAMERA_IDX as _; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + pi.resolutions = Some(SupportedResolutions { + resolutions: camera::Cameras::get_camera_resolution( + pi.current_display as usize, + ) + .ok() + .into_iter() + .collect(), + ..Default::default() + }) + .into(); + } + res.set_peer_info(pi); + self.update_codec_on_login(); } else { let supported_encoding = scrap::codec::Encoder::supported_encoding(); self.last_supported_encoding = Some(supported_encoding.clone()); @@ -1414,15 +1459,31 @@ impl Connection { } else { self.delayed_read_dir = Some((dir.to_owned(), show_hidden)); } + } else if self.view_camera { + if !wait_session_id_confirm { + self.try_sub_camera_displays(); + } + self.keyboard = false; + self.send_permission(Permission::Keyboard, false).await; } else if sub_service { if !wait_session_id_confirm { - self.try_sub_services(); + self.try_sub_monitor_services(); } } } - fn try_sub_services(&mut self) { - let is_remote = self.file_transfer.is_none() && self.port_forward_socket.is_none(); + fn try_sub_camera_displays(&mut self) { + if let Some(s) = self.server.upgrade() { + let mut s = s.write().unwrap(); + + s.try_add_primary_camera_service(); + s.add_camera_connection(self.inner.clone()); + } + } + + fn try_sub_monitor_services(&mut self) { + let is_remote = + self.file_transfer.is_none() && self.port_forward_socket.is_none() && !self.view_camera; if is_remote && !self.services_subed { self.services_subed = true; if let Some(s) = self.server.upgrade() { @@ -1466,7 +1527,7 @@ impl Connection { if let Some(current_sid) = crate::platform::get_current_process_session_id() { if crate::platform::is_installed() && crate::platform::is_share_rdp() - && raii::AuthedConnID::remote_and_file_conn_count() == 1 + && raii::AuthedConnID::non_port_forward_conn_count() == 1 && sessions.len() > 1 && sessions.iter().any(|e| e.sid == current_sid) && get_version_number(&self.lr.version) >= get_version_number("1.2.4") @@ -1539,6 +1600,7 @@ impl Connection { self.send_to_cm(ipc::Data::Login { id: self.inner.id(), is_file_transfer: self.file_transfer.is_some(), + is_view_camera: self.view_camera, port_forward: self.port_forward_address.clone(), peer_id, name, @@ -1781,6 +1843,15 @@ impl Connection { } self.file_transfer = Some((ft.dir, ft.show_hidden)); } + Some(login_request::Union::ViewCamera(_vc)) => { + if !Connection::permission(keys::OPTION_ENABLE_CAMERA) { + self.send_login_error("No permission of viewing camera") + .await; + sleep(1.).await; + return false; + } + self.view_camera = true; + } Some(login_request::Union::PortForward(mut pf)) => { if !Connection::permission("enable-tunnel") { self.send_login_error("No permission of IP tunneling").await; @@ -1987,6 +2058,9 @@ impl Connection { match msg.union { #[allow(unused_mut)] Some(message::Union::MouseEvent(mut me)) => { + if self.is_authed_view_camera_conn() { + return true; + } #[cfg(any(target_os = "android", target_os = "ios"))] if let Err(e) = call_main_service_pointer_input("mouse", me.mask, me.x, me.y) { log::debug!("call_main_service_pointer_input fail:{}", e); @@ -2005,6 +2079,9 @@ impl Connection { self.update_auto_disconnect_timer(); } Some(message::Union::PointerDeviceEvent(pde)) => { + if self.is_authed_view_camera_conn() { + return true; + } #[cfg(any(target_os = "android", target_os = "ios"))] if let Err(e) = match pde.union { Some(pointer_device_event::Union::TouchEvent(touch)) => match touch.union { @@ -2044,6 +2121,9 @@ impl Connection { Some(message::Union::KeyEvent(..)) => {} #[cfg(any(target_os = "android"))] Some(message::Union::KeyEvent(mut me)) => { + if self.is_authed_view_camera_conn() { + return true; + } let key = match me.mode.enum_value() { Ok(KeyboardMode::Map) => { Some(crate::keyboard::keycode_to_rdev_key(me.chr())) @@ -2096,6 +2176,9 @@ impl Connection { } #[cfg(not(any(target_os = "android", target_os = "ios")))] Some(message::Union::KeyEvent(me)) => { + if self.is_authed_view_camera_conn() { + return true; + } if self.peer_keyboard_enabled() { if is_enter(&me) { CLICK_TIME.store(get_time(), Ordering::SeqCst); @@ -2592,7 +2675,7 @@ impl Connection { let sessions = crate::platform::get_available_sessions(false); if crate::platform::is_installed() && crate::platform::is_share_rdp() - && raii::AuthedConnID::remote_and_file_conn_count() == 1 + && raii::AuthedConnID::non_port_forward_conn_count() == 1 && sessions.len() > 1 && current_process_sid != sid && sessions.iter().any(|e| e.sid == sid) @@ -2606,15 +2689,19 @@ impl Connection { if let Some((dir, show_hidden)) = self.delayed_read_dir.take() { self.read_dir(&dir, show_hidden); } + } else if self.view_camera { + self.try_sub_camera_displays(); } else { - self.try_sub_services(); + self.try_sub_monitor_services(); } } } Some(misc::Union::MessageQuery(mq)) => { - if let Some(msg_out) = - video_service::make_display_changed_msg(mq.switch_display as _, None) - { + if let Some(msg_out) = video_service::make_display_changed_msg( + mq.switch_display as _, + None, + self.video_source(), + ) { self.send(msg_out).await; } } @@ -2713,7 +2800,7 @@ impl Connection { video_service::refresh(); self.server.upgrade().map(|s| { s.read().unwrap().set_video_service_opt( - display, + display.map(|d| (self.video_source(), d)), video_service::OPTION_REFRESH, super::service::SERVICE_OPTION_VALUE_TRUE, ); @@ -2743,19 +2830,33 @@ impl Connection { // 1. For compatibility with old versions ( < 1.2.4 ). // 2. Sciter version. // 3. Update `SupportedResolutions`. - if let Some(msg_out) = video_service::make_display_changed_msg(self.display_idx, None) { + if let Some(msg_out) = + video_service::make_display_changed_msg(self.display_idx, None, self.video_source()) + { self.send(msg_out).await; } } } + fn video_source(&self) -> VideoSource { + if self.view_camera { + VideoSource::Camera + } else { + VideoSource::Monitor + } + } + fn switch_display_to(&mut self, display_idx: usize, server: Arc>) { - let new_service_name = video_service::get_service_name(display_idx); - let old_service_name = video_service::get_service_name(self.display_idx); + let new_service_name = video_service::get_service_name(self.video_source(), display_idx); + let old_service_name = + video_service::get_service_name(self.video_source(), self.display_idx); let mut lock = server.write().unwrap(); if display_idx != *display_service::PRIMARY_DISPLAY_IDX { if !lock.contains(&new_service_name) { - lock.add_service(Box::new(video_service::new(display_idx))); + lock.add_service(Box::new(video_service::new( + self.video_source(), + display_idx, + ))); } } // For versions greater than 1.2.4, a `CaptureDisplays` message will be sent immediately. @@ -2790,26 +2891,27 @@ impl Connection { } async fn capture_displays(&mut self, add: &[usize], sub: &[usize], set: &[usize]) { + let video_source = self.video_source(); if let Some(sever) = self.server.upgrade() { let mut lock = sever.write().unwrap(); for display in add.iter() { - let service_name = video_service::get_service_name(*display); + let service_name = video_service::get_service_name(video_source, *display); if !lock.contains(&service_name) { - lock.add_service(Box::new(video_service::new(*display))); + lock.add_service(Box::new(video_service::new(video_source, *display))); } } for display in set.iter() { - let service_name = video_service::get_service_name(*display); + let service_name = video_service::get_service_name(video_source, *display); if !lock.contains(&service_name) { - lock.add_service(Box::new(video_service::new(*display))); + lock.add_service(Box::new(video_service::new(video_source, *display))); } } if !add.is_empty() { - lock.capture_displays(self.inner.clone(), add, true, false); + lock.capture_displays(self.inner.clone(), video_source, add, true, false); } else if !sub.is_empty() { - lock.capture_displays(self.inner.clone(), sub, false, true); + lock.capture_displays(self.inner.clone(), video_source, sub, false, true); } else { - lock.capture_displays(self.inner.clone(), set, true, true); + lock.capture_displays(self.inner.clone(), video_source, set, true, true); } self.multi_ui_session = lock.get_subbed_displays_count(self.inner.id()) > 1; if self.follow_remote_window { @@ -2931,6 +3033,16 @@ impl Connection { self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } self.send(msg).await; + self.voice_calling = accepted; + if self.is_authed_view_camera_conn() { + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + self.inner.clone(), + self.audio_enabled() && accepted, + ); + } + } } else { log::warn!("Possible a voice call attack."); } @@ -2940,6 +3052,14 @@ impl Connection { crate::audio_service::set_voice_call_input_device(None, true); // Notify the connection manager that the voice call has been closed. self.send_to_cm(Data::CloseVoiceCall("".to_owned())); + self.voice_calling = false; + if self.is_authed_view_camera_conn() { + if let Some(s) = self.server.upgrade() { + s.write() + .unwrap() + .subscribe(super::audio_service::NAME, self.inner.clone(), false); + } + } } async fn update_options(&mut self, o: &OptionMessage) { @@ -3016,11 +3136,21 @@ impl Connection { if q != BoolOption::NotSet { self.disable_audio = q == BoolOption::Yes; if let Some(s) = self.server.upgrade() { - s.write().unwrap().subscribe( - super::audio_service::NAME, - self.inner.clone(), - self.audio_enabled(), - ); + if self.is_authed_view_camera_conn() { + if self.voice_calling || !self.audio_enabled() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + self.inner.clone(), + self.audio_enabled(), + ); + } + } else { + s.write().unwrap().subscribe( + super::audio_service::NAME, + self.inner.clone(), + self.audio_enabled(), + ); + } } } } @@ -3316,6 +3446,7 @@ impl Connection { fn portable_check(&mut self) { if self.portable.is_installed || self.file_transfer.is_some() + || self.view_camera || self.port_forward_socket.is_some() || !self.keyboard { @@ -3463,6 +3594,13 @@ impl Connection { false } + fn is_authed_view_camera_conn(&self) -> bool { + if let Some(id) = self.authed_conn_id.as_ref() { + return id.conn_type() == AuthConnType::ViewCamera; + } + false + } + #[cfg(feature = "unix-file-copy-paste")] async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) { let is_stopping_allowed = clip.is_stopping_allowed(); @@ -3966,7 +4104,6 @@ impl Retina { } mod raii { - // CONN_COUNT: remote connection count in fact // ALIVE_CONNS: all connections, including unauthorized connections // AUTHED_CONNS: all authorized connections @@ -4001,7 +4138,7 @@ mod raii { _ONCE.call_once(|| { shutdown_hooks::add_shutdown_hook(connection_shutdown_hook); }); - if conn_type == AuthConnType::Remote { + if conn_type == AuthConnType::Remote || conn_type == AuthConnType::ViewCamera { video_service::VIDEO_QOS .lock() .unwrap() @@ -4024,12 +4161,12 @@ mod raii { .send((conn_count, remote_count))); } - pub fn remote_and_file_conn_count() -> usize { + pub fn non_port_forward_conn_count() -> usize { AUTHED_CONNS .lock() .unwrap() .iter() - .filter(|c| c.1 == AuthConnType::Remote || c.1 == AuthConnType::FileTransfer) + .filter(|c| c.1 != AuthConnType::PortForward) .count() } @@ -4112,7 +4249,7 @@ mod raii { impl Drop for AuthedConnID { fn drop(&mut self) { - if self.1 == AuthConnType::Remote { + if self.1 == AuthConnType::Remote || self.1 == AuthConnType::ViewCamera { scrap::codec::Encoder::update(scrap::codec::EncodingUpdate::Remove(self.0)); video_service::VIDEO_QOS .lock() diff --git a/src/server/display_service.rs b/src/server/display_service.rs index 98b42a5face..33f6650189b 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -404,6 +404,7 @@ fn no_displays(displays: &Vec) -> bool { } } + #[inline] #[cfg(not(windows))] pub fn try_get_displays() -> ResultType> { diff --git a/src/server/input_service.rs b/src/server/input_service.rs index c7f651e9ac7..257f4d71b4f 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -501,8 +501,13 @@ pub fn try_start_record_cursor_pos() -> Option> { } pub fn try_stop_record_cursor_pos() { - let count_lock = CONN_COUNT.lock().unwrap(); - if *count_lock > 0 { + let remote_count = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.1 == AuthConnType::Remote) + .count(); + if remote_count > 0 { return; } RECORD_CURSOR_POS_RUNNING.store(false, Ordering::SeqCst); diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 47d2f578963..b8cba71dd7c 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -717,7 +717,7 @@ pub mod client { } let frame_ptr = base.add(ADDR_CAPTURE_FRAME); let data = slice::from_raw_parts(frame_ptr, (*frame_info).length); - Ok(Frame::PixelBuffer(PixelBuffer::new( + Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( data, self.width, self.height, @@ -808,8 +808,13 @@ pub mod client { }, ConnCount(None) => { if !quick_support { - let cnt = crate::server::CONN_COUNT.lock().unwrap().clone(); - stream.send(&Data::DataPortableService(ConnCount(Some(cnt)))).await.ok(); + let remote_count = crate::server::AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.1 == crate::server::AuthConnType::Remote) + .count(); + stream.send(&Data::DataPortableService(ConnCount(Some(remote_count)))).await.ok(); } }, WillClose => { diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs index 108af9f4498..344fc8548f9 100644 --- a/src/server/video_qos.rs +++ b/src/server/video_qos.rs @@ -106,7 +106,7 @@ pub struct VideoQoS { fps: u32, ratio: f32, users: HashMap, - displays: HashMap, + displays: HashMap, bitrate_store: u32, adjust_ratio_instant: Instant, abr_config: bool, @@ -168,8 +168,8 @@ impl VideoQoS { self.users.iter().any(|u| u.1.record) } - pub fn set_support_changing_quality(&mut self, display_idx: usize, support: bool) { - if let Some(display) = self.displays.get_mut(&display_idx) { + pub fn set_support_changing_quality(&mut self, video_service_name: &str, support: bool) { + if let Some(display) = self.displays.get_mut(video_service_name) { display.support_changing_quality = support; } } @@ -346,16 +346,17 @@ impl VideoQoS { // Common adjust functions impl VideoQoS { - pub fn new_display(&mut self, display_idx: usize) { - self.displays.insert(display_idx, DisplayData::default()); + pub fn new_display(&mut self, video_service_name: String) { + self.displays + .insert(video_service_name, DisplayData::default()); } - pub fn remove_display(&mut self, display_idx: usize) { - self.displays.remove(&display_idx); + pub fn remove_display(&mut self, video_service_name: &str) { + self.displays.remove(video_service_name); } - pub fn update_display_data(&mut self, display_idx: usize, send_counter: usize) { - if let Some(display) = self.displays.get_mut(&display_idx) { + pub fn update_display_data(&mut self, video_service_name: &str, send_counter: usize) { + if let Some(display) = self.displays.get_mut(video_service_name) { display.send_counter += send_counter; } self.adjust_fps(); diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 5bc58da45d3..04f63fe75b4 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -18,12 +18,7 @@ // to-do: // https://slhck.info/video/2017/03/01/rate-control.html -use super::{ - display_service::{check_display_changed, get_display_info}, - service::ServiceTmpl, - video_qos::VideoQoS, - *, -}; +use super::{display_service::check_display_changed, service::ServiceTmpl, video_qos::VideoQoS, *}; #[cfg(target_os = "linux")] use crate::common::SimpleCallOnReturn; #[cfg(target_os = "linux")] @@ -65,7 +60,6 @@ use std::{ time::{self, Duration, Instant}, }; -pub const NAME: &'static str = "video"; pub const OPTION_REFRESH: &'static str = "refresh"; lazy_static::lazy_static! { @@ -133,10 +127,34 @@ impl VideoFrameController { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum VideoSource { + Monitor, + Camera, +} + +impl VideoSource { + pub fn service_name_prefix(&self) -> &'static str { + match self { + VideoSource::Monitor => "monitor", + VideoSource::Camera => "camera", + } + } + + pub fn is_monitor(&self) -> bool { + matches!(self, VideoSource::Monitor) + } + + pub fn is_camera(&self) -> bool { + matches!(self, VideoSource::Camera) + } +} + #[derive(Clone)] pub struct VideoService { sp: GenericService, idx: usize, + source: VideoSource, } impl Deref for VideoService { @@ -153,14 +171,15 @@ impl DerefMut for VideoService { } } -pub fn get_service_name(idx: usize) -> String { - format!("{}{}", NAME, idx) +pub fn get_service_name(source: VideoSource, idx: usize) -> String { + format!("{}{}", source.service_name_prefix(), idx) } -pub fn new(idx: usize) -> GenericService { +pub fn new(source: VideoSource, idx: usize) -> GenericService { let vs = VideoService { - sp: GenericService::new(get_service_name(idx), true), + sp: GenericService::new(get_service_name(source, idx), true), idx, + source, }; GenericService::run(&vs, run); vs.sp @@ -292,7 +311,10 @@ impl DerefMut for CapturerInfo { } } -fn get_capturer(current: usize, portable_service_running: bool) -> ResultType { +fn get_capturer_monitor( + current: usize, + portable_service_running: bool, +) -> ResultType { #[cfg(target_os = "linux")] { if !is_x11() { @@ -309,6 +331,7 @@ fn get_capturer(current: usize, portable_service_running: bool) -> ResultType ResultType ResultType { + let cameras = camera::Cameras::get_sync_cameras(); + let ncamera = cameras.len(); + if ncamera <= current { + bail!("Failed to get camera {}, cameras len: {}", current, ncamera,); + } + let Some(camera) = cameras.get(current) else { + bail!( + "Camera of index {} doesn't exist or platform not supported", + current + ); + }; + let capturer = camera::Cameras::get_capturer(current)?; + let (width, height) = (camera.width as usize, camera.height as usize); + let origin = (camera.x as i32, camera.y as i32); + let name = &camera.name; + let privacy_mode_id = get_privacy_mode_conn_id().unwrap_or(INVALID_PRIVACY_MODE_CONN_ID); + let _capturer_privacy_mode_id = privacy_mode_id; + log::debug!( + "#cameras={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}, name:{}", + ncamera, + current, + &origin, + width, + height, + num_cpus::get_physical(), + num_cpus::get(), + name, + ); + return Ok(CapturerInfo { + origin, + width, + height, + ndisplay: ncamera, + current, + privacy_mode_id, + _capturer_privacy_mode_id: privacy_mode_id, + capturer, + }); +} +fn get_capturer( + source: VideoSource, + current: usize, + portable_service_running: bool, +) -> ResultType { + match source { + VideoSource::Monitor => get_capturer_monitor(current, portable_service_running), + VideoSource::Camera => get_capturer_camera(current), + } +} + fn run(vs: VideoService) -> ResultType<()> { - let _raii = Raii::new(vs.idx); + let _raii = Raii::new(vs.sp.name()); // Wayland only support one video capturer for now. It is ok to call ensure_inited() here. // // ensure_inited() is needed because clear() may be called. @@ -406,7 +480,7 @@ fn run(vs: VideoService) -> ResultType<()> { let display_idx = vs.idx; let sp = vs.sp; - let mut c = get_capturer(display_idx, last_portable_service_running)?; + let mut c = get_capturer(vs.source, display_idx, last_portable_service_running)?; #[cfg(windows)] if !scrap::codec::enable_directx_capture() && !c.is_gdi() { log::info!("disable dxgi with option, fall back to gdi"); @@ -423,11 +497,12 @@ fn run(vs: VideoService) -> ResultType<()> { drop(video_qos); let (mut encoder, encoder_cfg, codec_format, use_i444, recorder) = match setup_encoder( &c, - display_idx, + sp.name(), quality, client_record, record_incoming, last_portable_service_running, + vs.source, ) { Ok(result) => result, Err(err) => { @@ -441,26 +516,29 @@ fn run(vs: VideoService) -> ResultType<()> { })); setup_encoder( &c, - display_idx, + sp.name(), quality, client_record, record_incoming, last_portable_service_running, + vs.source, )? } }; #[cfg(feature = "vram")] c.set_output_texture(encoder.input_texture()); #[cfg(target_os = "android")] - if let Err(e) = check_change_scale(encoder.is_hardware()) { - try_broadcast_display_changed(&sp, display_idx, &c, true).ok(); - bail!(e); + if vs.source.is_monitor() { + if let Err(e) = check_change_scale(encoder.is_hardware()) { + try_broadcast_display_changed(&sp, display_idx, &c, true).ok(); + bail!(e); + } } VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate()); VIDEO_QOS .lock() .unwrap() - .set_support_changing_quality(display_idx, encoder.support_changing_quality()); + .set_support_changing_quality(&sp.name(), encoder.support_changing_quality()); log::info!("initial quality: {quality:?}"); if sp.is_option_true(OPTION_REFRESH) { @@ -500,10 +578,12 @@ fn run(vs: VideoService) -> ResultType<()> { client_record, &mut send_counter, &mut second_instant, - display_idx, + &sp.name(), )?; if sp.is_option_true(OPTION_REFRESH) { - let _ = try_broadcast_display_changed(&sp, display_idx, &c, true); + if vs.source.is_monitor() { + let _ = try_broadcast_display_changed(&sp, display_idx, &c, true); + } log::info!("switch to refresh"); bail!("SWITCH"); } @@ -527,10 +607,12 @@ fn run(vs: VideoService) -> ResultType<()> { #[cfg(all(windows, feature = "vram"))] if c.is_gdi() && encoder.input_texture() { log::info!("changed to gdi when using vram"); - VRamEncoder::set_fallback_gdi(display_idx, true); + VRamEncoder::set_fallback_gdi(sp.name(), true); bail!("SWITCH"); } - check_privacy_mode_changed(&sp, display_idx, &c)?; + if vs.source.is_monitor() { + check_privacy_mode_changed(&sp, display_idx, &c)?; + } #[cfg(windows)] { if crate::platform::windows::desktop_changed() @@ -540,7 +622,7 @@ fn run(vs: VideoService) -> ResultType<()> { } } let now = time::Instant::now(); - if last_check_displays.elapsed().as_millis() > 1000 { + if vs.source.is_monitor() && last_check_displays.elapsed().as_millis() > 1000 { last_check_displays = now; // This check may be redundant, but it is better to be safe. // The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough. @@ -575,7 +657,7 @@ fn run(vs: VideoService) -> ResultType<()> { { #[cfg(feature = "vram")] if try_gdi == 1 && !c.is_gdi() { - VRamEncoder::set_fallback_gdi(display_idx, false); + VRamEncoder::set_fallback_gdi(sp.name(), false); } try_gdi = 0; } @@ -635,7 +717,9 @@ fn run(vs: VideoService) -> ResultType<()> { Err(err) => { // This check may be redundant, but it is better to be safe. // The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough. - try_broadcast_display_changed(&sp, display_idx, &c, true)?; + if vs.source.is_monitor() { + try_broadcast_display_changed(&sp, display_idx, &c, true)?; + } #[cfg(windows)] if !c.is_gdi() { @@ -657,7 +741,9 @@ 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, display_idx, &c)?; + if vs.source.is_monitor() { + 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() { @@ -676,32 +762,35 @@ fn run(vs: VideoService) -> ResultType<()> { Ok(()) } -struct Raii(usize); +struct Raii(String); impl Raii { - fn new(display_idx: usize) -> Self { - VIDEO_QOS.lock().unwrap().new_display(display_idx); - Raii(display_idx) + fn new(name: String) -> Self { + log::info!("new video service: {}", name); + VIDEO_QOS.lock().unwrap().new_display(name.clone()); + Raii(name) } } impl Drop for Raii { fn drop(&mut self) { + log::info!("stop video service: {}", self.0); #[cfg(feature = "vram")] - VRamEncoder::set_not_use(self.0, false); + VRamEncoder::set_not_use(self.0.clone(), false); #[cfg(feature = "vram")] Encoder::update(scrap::codec::EncodingUpdate::Check); - VIDEO_QOS.lock().unwrap().remove_display(self.0); + VIDEO_QOS.lock().unwrap().remove_display(&self.0); } } fn setup_encoder( c: &CapturerInfo, - display_idx: usize, + name: String, quality: f32, client_record: bool, record_incoming: bool, last_portable_service_running: bool, + source: VideoSource, ) -> ResultType<( Encoder, EncoderCfg, @@ -711,14 +800,15 @@ fn setup_encoder( )> { let encoder_cfg = get_encoder_config( &c, - display_idx, + name.to_string(), quality, client_record || record_incoming, last_portable_service_running, + source, ); Encoder::set_fallback(&encoder_cfg); let codec_format = Encoder::negotiated_codec(); - let recorder = get_recorder(record_incoming, display_idx); + let recorder = get_recorder(record_incoming, name); 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)) @@ -726,15 +816,16 @@ fn setup_encoder( fn get_encoder_config( c: &CapturerInfo, - _display_idx: usize, + _name: String, quality: f32, record: bool, _portable_service: bool, + _source: VideoSource, ) -> EncoderCfg { #[cfg(all(windows, feature = "vram"))] - if _portable_service || c.is_gdi() { + if _portable_service || c.is_gdi() || _source == VideoSource::Camera { log::info!("gdi:{}, portable:{}", c.is_gdi(), _portable_service); - VRamEncoder::set_not_use(_display_idx, true); + VRamEncoder::set_not_use(_name, true); } #[cfg(feature = "vram")] Encoder::update(scrap::codec::EncodingUpdate::Check); @@ -800,7 +891,7 @@ fn get_encoder_config( } } -fn get_recorder(record_incoming: bool, display: usize) -> Arc>> { +fn get_recorder(record_incoming: bool, video_service_name: String) -> Arc>> { #[cfg(windows)] let root = crate::platform::is_root(); #[cfg(not(windows))] @@ -819,7 +910,7 @@ fn get_recorder(record_incoming: bool, display: usize) -> Arc, + source: VideoSource, ) -> Option { let display = match opt_display { Some(d) => d, - None => get_display_info(display_idx)?, + None => match source { + VideoSource::Monitor => display_service::get_display_info(display_idx)?, + VideoSource::Camera => camera::Cameras::get_sync_cameras() + .get(display_idx)? + .clone(), + }, }; let mut misc = Misc::new(); misc.set_switch_display(SwitchDisplay { @@ -1033,13 +1132,24 @@ pub fn make_display_changed_msg( y: display.y, width: display.width, height: display.height, - cursor_embedded: display_service::capture_cursor_embedded(), + cursor_embedded: match source { + VideoSource::Monitor => display_service::capture_cursor_embedded(), + VideoSource::Camera => false, + }, #[cfg(not(target_os = "android"))] resolutions: Some(SupportedResolutions { - resolutions: if display.name.is_empty() { - vec![] - } else { - crate::platform::resolutions(&display.name) + resolutions: match source { + VideoSource::Monitor => { + if display.name.is_empty() { + vec![] + } else { + crate::platform::resolutions(&display.name) + } + } + VideoSource::Camera => camera::Cameras::get_camera_resolution(display_idx) + .ok() + .into_iter() + .collect(), }, ..SupportedResolutions::default() }) @@ -1059,7 +1169,7 @@ fn check_qos( client_record: bool, send_counter: &mut usize, second_instant: &mut Instant, - display_idx: usize, + name: &str, ) -> ResultType<()> { let mut video_qos = VIDEO_QOS.lock().unwrap(); *spf = video_qos.spf(); @@ -1082,7 +1192,7 @@ fn check_qos( } if second_instant.elapsed() > Duration::from_secs(1) { *second_instant = Instant::now(); - video_qos.update_display_data(display_idx, *send_counter); + video_qos.update_display_data(&name, *send_counter); *send_counter = 0; } drop(video_qos); diff --git a/src/ui/cm.rs b/src/ui/cm.rs index c8c8c657fb7..5e56896affb 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -19,6 +19,7 @@ impl InvokeUiCM for SciterHandler { &make_args!( client.id, client.is_file_transfer, + client.is_view_camera, client.port_forward.clone(), client.peer_id.clone(), client.name.clone(), diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 38f5c5c2d80..fc18bdfd30a 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -356,7 +356,7 @@ function bring_to_top(idx=-1) { } } -handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { +handler.addConnection = function(id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { stdout.println("new connection #" + id + ": " + peer_id); var conn; connections.map(function(c) { @@ -550,7 +550,7 @@ function adjustHeader() { view.on("size", adjustHeader); -// handler.addConnection(0, false, 0, "", "test1", true, false, false, true, true); -// handler.addConnection(1, false, 0, "", "test2--------", true, false, false, false, false); -// handler.addConnection(2, false, 0, "", "test3", true, false, false, false, false); +// handler.addConnection(0, false, false, 0, "", "test1", true, false, false, true, true); +// handler.addConnection(1, false, false, 0, "", "test2--------", true, false, false, false, false); +// handler.addConnection(2, false, false, 0, "", "test3", true, false, false, false, false); // handler.newMessage(0, 'h'); diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 8bcc9cc1b86..a1c0369723c 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -316,6 +316,7 @@ impl InvokeUiSession for SciterHandler { ConnType::RDP => {} ConnType::PORT_FORWARD => {} ConnType::FILE_TRANSFER => {} + ConnType::VIEW_CAMERA => {} ConnType::DEFAULT_CONN => { crate::keyboard::client::start_grab_loop(); } @@ -557,6 +558,8 @@ impl SciterSession { let conn_type = if cmd.eq("--file-transfer") { ConnType::FILE_TRANSFER + } else if cmd.eq("--view-camera") { + ConnType::VIEW_CAMERA } else if cmd.eq("--port-forward") { ConnType::PORT_FORWARD } else if cmd.eq("--rdp") { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index c1e25362a84..b36c37be79d 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -47,6 +47,7 @@ pub struct Client { pub authorized: bool, pub disconnected: bool, pub is_file_transfer: bool, + pub is_view_camera: bool, pub port_forward: String, pub name: String, pub peer_id: String, @@ -128,6 +129,7 @@ impl ConnectionManager { &self, id: i32, is_file_transfer: bool, + is_view_camera: bool, port_forward: String, peer_id: String, name: String, @@ -147,6 +149,7 @@ impl ConnectionManager { authorized, disconnected: false, is_file_transfer, + is_view_camera, port_forward, name: name.clone(), peer_id: peer_id.clone(), @@ -402,9 +405,9 @@ impl IpcTaskRunner { } Ok(Some(data)) => { match data { - Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { + Data::Login{id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { log::debug!("conn_id: {}", id); - self.cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); + self.cm.add_connection(id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); self.conn_id = id; #[cfg(target_os = "windows")] { @@ -672,6 +675,7 @@ pub async fn start_listen( Some(Data::Login { id, is_file_transfer, + is_view_camera, port_forward, peer_id, name, @@ -690,6 +694,7 @@ pub async fn start_listen( cm.add_connection( id, is_file_transfer, + is_view_camera, port_forward, peer_id, name, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index f2797876a19..b79ca72c29b 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -190,6 +190,10 @@ impl Session { .eq(&ConnType::FILE_TRANSFER) } + pub fn is_view_camera(&self) -> bool { + self.lc.read().unwrap().conn_type.eq(&ConnType::VIEW_CAMERA) + } + pub fn is_port_forward(&self) -> bool { let conn_type = self.lc.read().unwrap().conn_type; conn_type == ConnType::PORT_FORWARD || conn_type == ConnType::RDP @@ -1630,7 +1634,12 @@ impl Interface for Session { if pi.displays.is_empty() { self.lc.write().unwrap().handle_peer_info(&pi); self.update_privacy_mode(); - self.msgbox("error", "Remote Error", "No Displays", ""); + let msg = if self.is_view_camera() { + "No cameras" + } else { + "No displays" + }; + self.msgbox("error", "Error", msg, ""); return; } self.try_change_init_resolution(pi.current_display); From 5f521c80a71b2b247f1c9e6f580b69fe57d9d839 Mon Sep 17 00:00:00 2001 From: Naveenkumar <38713995+naveendev97@users.noreply.github.com> Date: Mon, 10 Mar 2025 18:37:15 +0530 Subject: [PATCH 139/506] Create ta.rs (#11068) * Create ta.rs * Update lang.rs --- src/lang.rs | 3 + src/lang/ta.rs | 661 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 664 insertions(+) create mode 100644 src/lang/ta.rs diff --git a/src/lang.rs b/src/lang.rs index 54f06d87499..de72745f379 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -44,6 +44,7 @@ mod tr; mod tw; mod uk; mod vn; +mod ta; pub const LANGS: &[(&str, &str)] = &[ ("en", "English"), @@ -89,6 +90,7 @@ pub const LANGS: &[(&str, &str)] = &[ ("he", "עברית"), ("hr", "Hrvatski"), ("sc", "Sardu"), + ("ta", "தமிழ்"), ]; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -164,6 +166,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "he" => he::T.deref(), "hr" => hr::T.deref(), "sc" => sc::T.deref(), + "ta" => ta::T.deref(), _ => en::T.deref(), }; let (name, placeholder_value) = extract_placeholder(&name); diff --git a/src/lang/ta.rs b/src/lang/ta.rs new file mode 100644 index 00000000000..2cf3f1c7030 --- /dev/null +++ b/src/lang/ta.rs @@ -0,0 +1,661 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "நிலை"), + ("Your Desktop", "உங்கள் டெஸ்க்டாப்"), + ("desk_tip", "டெஸ்க்டாப் பயன்ப்புத்தகம்"), + ("Password", "கடவுச்சொல்"), + ("Ready", "தயார்"), + ("Established", "நிறைவேற்றம்"), + ("connecting_status", "இணைப்பு நிலை"), + ("Enable service", "சேவையை இயக்கு"), + ("Start service", "சேவையை தொடங்கு"), + ("Service is running", "சேவை இயங்குகிறது"), + ("Service is not running", "சேவை இயங்கவில்லை."), + ("not_ready_status", "இயக்கம் இல்லை"), + ("Control Remote Desktop", "தொலைதூர டெஸ்க்டாப் கட்டுப்பாடு"), + ("Transfer file", "கோப்பு பரிமாற்றம்"), + ("Connect", "இணைக்க"), + ("Recent sessions", "கடந்த அமர்வுகள்"), + ("Address book", "முகவர்கள் புல்லிட்டு"), + ("Confirmation", "உறுதிப்படுத்தல்"), + ("TCP tunneling", "TCP டன்னலிங்"), + ("Remove", "அகற்று"), + ("Refresh random password", "சீரற்ற கடவுச்சொல்லைப் புதுப்பிக்கவும்"), + ("Set your own password", "உங்கள் தனிப்பட்ட கடவுச்சொல்லை அமைக்கவும்"), + ("Enable keyboard/mouse", ""), + ("Enable clipboard", "கிளிப்போர்டை இயக்கு"), + ("Enable file transfer", "கோப்பு பரிமாற்றத்தை இயக்கு"), + ("Enable TCP tunneling", "TCP டன்னலிங் இயக்கு"), + ("IP Whitelisting", "ஐபி அனுமதிப்பட்டியல்"), + ("ID/Relay Server", "ஐடி/ரிலே சர்வர்"), + ("Import server config", "சர்வர் உள்ளமைவு இறக்குமதி"), + ("Export Server Config", "சர்வர் உள்ளமைவு ஏற்றுமதி"), + ("Import server configuration successfully", "சர்வர் உள்ளமைவை வெற்றிகரமாக இறக்குமதி செய்யபட்டது"), + ("Export server configuration successfully", "சர்வர் உள்ளமைவை வெற்றிகரமாக ஏற்றுமதி செய்யபட்டது"), + ("Invalid server configuration", "தவறான சர்வர் கட்டமைப்பு"), + ("Clipboard is empty", "கிளிப்போர்டு காலியாக உள்ளது."), + ("Stop service", "சேவையை நிறுத்து"), + ("Change ID", "ஐடி மாற்று"), + ("Your new ID", "உங்கள் புதிய ஐடி"), + ("length %min% to %max%", "நீளம் %min% முதல் %max%"), + ("starts with a letter", "ஒரு எழுத்தால் தொடங்கு"), + ("allowed characters", "அனுமதிக்கப்பட்ட எழுத்துக்கள்"), + ("id_change_tip", "ஐடி_மாற்ற_குறிப்பு"), + ("Website", "இணையதளம்"), + ("About", "பற்றி"), + ("Slogan_tip", "முழக்கம்_குறிப்பு"), + ("Privacy Statement", "தனியுரிமை அறிக்கை"), + ("Mute", "ஒலியடக்கவும்"), + ("Build Date", "கட்டப்பட்ட தேதி"), + ("Version", "பதிப்பு"), + ("Home", "வீடு"), + ("Audio Input", "ஒலி உள்ளீடு"), + ("Enhancements", "மேம்பாடுகள்"), + ("Hardware Codec", "வன்பொருள் கோடெக்"), + ("Adaptive bitrate", "தகவமைப்பு பிட்ரேட்"), + ("ID Server", "ஐடி சர்வர்"), + ("Relay Server", "ரிலே சர்வர்"), + ("API Server", "API சர்வர்"), + ("invalid_http", "தவறான_http"), + ("Invalid IP", "தவறான IP"), + ("Invalid format", "தவறான வடிவம்"), + ("server_not_support", "சர்வர்_ஆதரவு_இல்லை"), + ("Not available", "இல்லை"), + ("Too frequent", "அடிக்கடி"), + ("Cancel", "ரத்துசெய்"), + ("Skip", "தவிர்"), + ("Close", "மூடு"), + ("Retry", "மீண்டும் முயலவும்"), + ("OK", "சரி"), + ("Password Required", "கடவுச்சொல்_தேவை"), + ("Please enter your password", "உங்கள் கடவுச்சொல்லை உள்ளிடுக"), + ("Remember password", "கடவுச்சொல்லை நினைவு கொள்"), + ("Wrong Password", "தவறான கடவுச்சொல்"), + ("Do you want to enter again?", "மீண்டும் முயலவுமா?"), + ("Connection Error", "இணைப்பு பிழை"), + ("Error", "பிழை"), + ("Reset by the peer", "பியர் மூலம் மீட்டமை"), + ("Connecting...", "இணைப்பு ..."), + ("Connection in progress. Please wait.", "இணைப்பு முயற்சிக்கின்றது. நீங்கள் காத்திருக்கவும்..."), + ("Please try 1 minute later", "1 நிமிடம் கழித்து முயற்சிக்கவும்."), + ("Login Error", "பதிவு பிழை"), + ("Successful", "வெற்றிகரம்"), + ("Connected, waiting for image...", "இணைக்கப்பட்டது, புகைப்படத்துக்காக காத்திருப்பு..."), + ("Name", "பெயர்"), + ("Type", "வகை"), + ("Modified", "மாற்றப்பட்டது"), + ("Size", "அளவு"), + ("Show Hidden Files", "முடக்கப்பட்ட கோப்புகளை காட்டு"), + ("Receive", "பெறு"), + ("Send", "அனுப்பு"), + ("Refresh File", "கோப்பு புதுப்பி"), + ("Local", "உள்ளூர்"), + ("Remote", "ரிமோட்"), + ("Remote Computer", "ரிமோட் கணினி"), + ("Local Computer", "உள்ளூர் கணினி"), + ("Confirm Delete", "நீக்குவதை உறுதிசெய்"), + ("Delete", "நீக்கு"), + ("Properties", "பண்புகள்"), + ("Multi Select", "பலவற்றை தேர்வு"), + ("Select All", "அனைத்தும் தேர்வு"), + ("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!", "இது மீண்டும் மீட்டமையாது!"), + ("Deleting", "நீக்குதல்"), + ("files", "கோப்புகள்"), + ("Waiting", "காத்திருக்கும்"), + ("Finished", "முடிந்தது"), + ("Speed", "வேகம்"), + ("Custom Image Quality", "தனிப்பட்ட புகைப்பட தரம்"), + ("Privacy mode", "தனியுரிமை முறை"), + ("Block user input", "பயனர் உள்ளீட்டைத் தடு"), + ("Unblock user input", "பயனர் உள்ளீட்டைத் தடைநீக்கு."), + ("Adjust Window", "சட்டகம் சரிசெய்யவும்"), + ("Original", "அசல்"), + ("Shrink", "குறுக்கு"), + ("Stretch", "நீட்டு"), + ("Scrollbar", "ஸ்க்ரோல் பட்டி"), + ("ScrollAuto", "ஸ்க்ரோல்ஆட்டோ"), + ("Good image quality", "நல்ல புகைப்பட தரம்"), + ("Balanced", "சமநிலை"), + ("Optimize reaction time", "எதிர்வினை நேரத்தை மேம்பாடு"), + ("Custom", "தனிப்பட்ட"), + ("Show remote cursor", "தொலைதூர கற்புக்குழுயை காட்டு"), + ("Show quality monitor", "புகைப்பட தரம் காட்டு"), + ("Disable clipboard", "கிளிப்போர்டை மறை"), + ("Lock after session end", "அமர்வு முடிவுக்குப் பின் மறை"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del செய்"), + ("Insert Lock", "மறை செய்"), + ("Refresh", "புதுப்பி"), + ("ID does not exist", "ஐடி இல்லை"), + ("Failed to connect to rendezvous server", "ரென்ட்வெர்ச் சர்வருக்கு இணைப்பு பிழை"), + ("Please try later", "பிறகு முயற்சிக்கவும்"), + ("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", "கடவுச்சொல்லை அமைக்கவும்"), + ("OS Password", "OS கடவுச்சொல்"), + ("install_tip", "நிறுவல்_குறிப்பு"), + ("Click to upgrade", "மேம்படுத்த கிளிக் செய்யவும்"), + ("Click to download", "பதிவிறக்க கிளிக் செய்யவும்"), + ("Click to update", "புதுப்பிக்க கிளிக் செய்யவும்"), + ("Configure", "உள்ளமை"), + ("config_acc", "உள்ளமைவு_அக்கெஸ்ஸ்"), + ("config_screen", "config_screen"), + ("Installing ...", "நிறுவுதல் ..."), + ("Install", "நிறுவு"), + ("Installation", "நிறுவல்"), + ("Installation Path", "நிறுவல் பாதை"), + ("Create start menu shortcuts", "தொடக்க மெனு ஷார்ட்கட்களை உருவாக்கவும்"), + ("Create desktop icon", "டெஸ்க்டாப் ஐகானை உருவாக்கு"), + ("agreement_tip", "ஒப்பந்தம்_குறிப்பு"), + ("Accept and Install", "ஏற்றுக்கொண்டு நிறுவு"), + ("End-user license agreement", "இறுதி-பயனர் உரிம ஒப்பந்தம்"), + ("Generating ...", "உருவாக்குதல் ..."), + ("Your installation is lower version.", "உங்கள் நிறுவல் கீழ் பதிப்பில் உள்ளது."), + ("not_close_tcp_tip", "not_close_tcp_tip_குறிப்பு"), + ("Listening ...", "கேட்கிறது..."), + ("Remote Host", "தொலை ஹோஸ்ட்"), + ("Remote Port", "தொலை போர்ட்"), + ("Action", "செயல்"), + ("Add", "சேர்"), + ("Local Port", "உள்ளூர் போர்ட்"), + ("Local Address", "உள்ளூர் முகவரி"), + ("Change Local Port", "உள்ளூர் போர்ட் மாற்று"), + ("setup_server_tip", "setup_server_tip_குறிப்பு"), + ("Too short, at least 6 characters.", "மிகக் குறுகியது, குறைந்தபட்சம் 6 எழுத்துக்கள்."), + ("The confirmation is not identical.", "உறுதிப்படுத்தல் ஒரே மாதிரியாக இல்லை."), + ("Permissions", "அனுமதிகள்"), + ("Accept", "ஏற்று"), + ("Dismiss", "ரத்து"), + ("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", "தொலை ஐடியை உள்ளிடு"), + ("Enter your password", "உங்கள் கடவுச்சொல்லை உள்ளிடு"), + ("Logging in...", "பதிவு முயற்சிக்கிறது..."), + ("Enable RDP session sharing", "RDP அமர்வு பகிர்வு இயக்கு"), + ("Auto Login", "தானியங்கு உள்நுழைவு"), + ("Enable direct IP access", "நேரடி IP அனுமதிப்பு இயக்கு"), + ("Rename", "பெயர் மாற்று"), + ("Space", "இடம்"), + ("Create desktop shortcut", "டெஸ்க்டாப் ஐகானை உருவாக்கு"), + ("Change Path", "பாதை மாற்று"), + ("Create Folder", "கோப்புக்குழு உருவாக்கு"), + ("Please enter the folder name", "கோப்புக்குழுவின் பெயரை உள்ளிடு"), + ("Fix it", "சரி செய்"), + ("Warning", "எச்சரிக்கை"), + ("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", "பியர் மூலம் மூடப்பட்டது"), + ("Enable remote configuration modification", "தொலை அமைப்பு மாற்று இயக்கு"), + ("Run without install", "நிறுவல் இல்லாமல் இயக்கு"), + ("Connect via relay", "ரிலே மூலம் இணைக்கவும்"), + ("Always connect via relay", "எப்போதும் ரிலே மூலம் இணைக்கவும்"), + ("whitelist_tip", "அனுமதிப்பட்டியல்_குறிப்பு"), + ("Login", "உள்நுழை"), + ("Verify", "உறுதிப்படுத்து"), + ("Remember me", "நினைவு கொள்"), + ("Trust this device", "இந்த சாதனத்தை நம்பு"), + ("Verification code", "சரிபார்ப்பு குறியீடு"), + ("verification_tip", "சரிபார்ப்பு_குறிப்பு"), + ("Logout", "வெளியேறு"), + ("Tags", "குறிச்சொற்கள்"), + ("Search ID", "ஐடி தேடு"), + ("whitelist_sep", "அனுமதிப்பட்டியல்_sep"), + ("Add ID", "ஐடி சேர்"), + ("Add Tag", "குறிச்சொற்கள் சேர்"), + ("Unselect all tags", "அனைத்து குறிச்சொற்களைத் தேர்வு நீக்கு"), + ("Network error", "நெட்வொர்க் பிழை"), + ("Username missed", "பயனர்பெயர் தவறவிட்டது"), + ("Password missed", "கடவுச்சொல் தவறவிட்டது"), + ("Wrong credentials", "தவறான சான்றுகள்"), + ("The verification code is incorrect or has expired", "சரிபார்ப்புக் குறியீடு தவறானது அல்லது காலாவதியானது"), + ("Edit Tag", "குறிச்சொற்கள் மாற்று"), + ("Forget Password", "கடவுச்சொல்லை மறந்துவிடு"), + ("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", "install_daemon_tip_குறிப்பு"), + ("Remote ID", "தொலை ஐடி"), + ("Paste", "பேஸ்ட்"), + ("Paste here?", "இங்கே பேஸ்ட் செய்?"), + ("Are you sure to close the connection?", "இணைப்பை மூடுவதை உறுதிசெய்?"), + ("Download new version", "புதிய பதிப்பை பதிவிறக்கு"), + ("Touch mode", ""), + ("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", ""), + ("Reset canvas", ""), + ("No permission of file transfer", ""), + ("Note", ""), + ("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", ""), + ("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", ""), + ("Exit Fullscreen", ""), + ("Fullscreen", ""), + ("Mobile Actions", ""), + ("Select Monitor", ""), + ("Control Actions", ""), + ("Display Settings", ""), + ("Ratio", ""), + ("Image Quality", ""), + ("Scroll Style", ""), + ("Show Toolbar", ""), + ("Hide Toolbar", ""), + ("Direct Connection", ""), + ("Relay Connection", ""), + ("Secure Connection", ""), + ("Insecure Connection", ""), + ("Scale original", ""), + ("Scale adaptive", ""), + ("General", ""), + ("Security", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Light Theme", ""), + ("Dark", ""), + ("Light", ""), + ("Follow System", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable audio", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Pin Toolbar", ""), + ("Unpin Toolbar", ""), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Automatically record outgoing 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...", ""), + ("elevated_foreground_window_tip", ""), + ("Disconnected", ""), + ("Other", ""), + ("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).", ""), + ("Show 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...", ""), + ("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", ""), + ("Right click to select tabs", ""), + ("Skipped", ""), + ("Add to address book", ""), + ("Group", ""), + ("Search", ""), + ("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", ""), + ("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", ""), + ("Download", ""), + ("Upload folder", ""), + ("Upload files", ""), + ("Clipboard is synchronized", ""), + ("Update client clipboard", ""), + ("Untagged", ""), + ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ].iter().cloned().collect(); +} From b2cc9eac237f054dc6621074ddc7ce50c43fd061 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 10 Mar 2025 21:12:13 +0800 Subject: [PATCH 140/506] set rgba_data.valid to false when open a new single display on the old session (#11078) Signed-off-by: 21pages --- src/flutter.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/flutter.rs b/src/flutter.rs index 2e86f6893f3..0ea552254b4 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -2036,6 +2036,8 @@ pub mod sessions { // This operation will also cause the peer to send a switch display message. // The switch display message will contain `SupportedResolutions`, which is useful when changing resolutions. s.switch_display(value[0]); + // Reset the valid flag of the display. + s.next_rgba(value[0] as usize); if !is_desktop { s.capture_displays(vec![], vec![], value); @@ -2113,6 +2115,11 @@ pub mod sessions { .write() .unwrap() .insert(session_id, h); + // If the session is a single display session, it may be a software rgba rendered display. + // If this is the second time the display is opened, the old valid flag may be true. + if displays.len() == 1 { + s.ui_handler.next_rgba(displays[0] as usize); + } true } else { false From e0fd6981018091a31bae2e883900306ebffc1ce1 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 11 Mar 2025 16:29:02 +0800 Subject: [PATCH 141/506] opt dropdown button of connection page (#11086) * Use menu style of the peer card * Add margin between connection button and dropdown button * Use thinner icon Signed-off-by: 21pages --- flutter/assets/more.ttf | Bin 0 -> 1620 bytes flutter/lib/common.dart | 3 + .../lib/desktop/pages/connection_page.dart | 100 +++++++++++------- .../lib/desktop/pages/view_camera_page.dart | 2 +- flutter/pubspec.yaml | 3 + 5 files changed, 71 insertions(+), 37 deletions(-) create mode 100644 flutter/assets/more.ttf diff --git a/flutter/assets/more.ttf b/flutter/assets/more.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3b01435df3ac47b3db64ea4196935c064dae987b GIT binary patch literal 1620 zcmd^9Jx>%t7=C7VIc^U{0*N9T*$_k{&Y;3J8G+J1gSlAdFdt+kINCE|xQVKq^I~P&-2fmki=Xsy^W8Rsa8wdbRs6ofT z*u>Ns>)z3ufH*~V^72G~ZurQB&$RE;zMl8&%G%)T_q2a)SZ(PEl_xj(4VZjiTeT-thpD&^UW6|onc^taQteA{zc zo({jHpZQv;5>z9CDZ=flOUDqVhsc0AsV>wUF{>e2gxkfkrT|+(4dpuMXnUj7w~S?Ts7nKfc^zFhW&jS znp{w|#5MTEfxd(ilPdNSS((+xnY$%#pev4HYrv|x#r-FmcCrHq+5i78d*TB)td1c* zD|u41ldmA}d5rW2ylJgKThEBd9Wcd+=oCjpqi7b#YH88hVTvg+k#5iErrx;u$UKzM zvSvqXdh?d3k! zso#Vikv|(ccH|VSnI&sBANa+7hquqOVqQ-BY=-$ ztfL+FIHOCLhs(@xhxgy?X-0>rGEUaCFvy2(U`N&%qx?AX-)^c{;A%Pc7_lRZBV;9y zXjJv*uoIWq;s=zzIo8QDI*W3gRlyMY`K#}z9-oc(>B)yD5&8b@CYsQM4#NvV`G(>* D@=?y^ literal 0 HcmV?d00001 diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index bf769d4945d..5d43d7ae1cc 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -108,6 +108,8 @@ class IconFont { static const _family2 = 'PeerSearchbar'; static const _family3 = 'AddressBook'; static const _family4 = 'DeviceGroup'; + static const _family5 = 'More'; + IconFont._(); static const IconData max = IconData(0xe606, fontFamily: _family1); @@ -123,6 +125,7 @@ class IconFont { IconData(0xe623, fontFamily: _family4); static const IconData deviceGroupFill = IconData(0xe748, fontFamily: _family4); + static const IconData more = IconData(0xe609, fontFamily: _family5); } class ColorThemeExtension extends ThemeExtension { diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 8efc355f31f..6089603783f 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -2,10 +2,12 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math'; 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/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -17,6 +19,7 @@ import '../../common/formatter/id_formatter.dart'; import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/autocomplete.dart'; import '../../models/platform_model.dart'; +import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; class OnlineStatusWidget extends StatefulWidget { const OnlineStatusWidget({Key? key, this.onSvcStatusChanged}) @@ -211,6 +214,8 @@ class _ConnectionPageState extends State // https://github.com/flutter/flutter/issues/157244 Iterable _autocompleteOpts = []; + final _menuOpen = false.obs; + @override void initState() { super.initState(); @@ -513,7 +518,7 @@ class _ConnectionPageState extends State child: Text(translate("Connect")), ), ), - const SizedBox(width: 3), + const SizedBox(width: 8), Container( height: 28.0, width: 28.0, @@ -522,41 +527,64 @@ class _ConnectionPageState extends State borderRadius: BorderRadius.circular(8), ), child: Center( - child: MenuAnchor( - builder: (context, controller, builder) { - return IconButton( - padding: EdgeInsets.zero, - constraints: BoxConstraints(), - visualDensity: VisualDensity.compact, - icon: controller.isOpen - ? const Icon(Icons.keyboard_arrow_up) - : const Icon(Icons.keyboard_arrow_down), - onPressed: () { - setState(() { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }); - }, - ); - }, - menuChildren: [ - MenuItemButton( - onPressed: () { - onConnect(isFileTransfer: true); - }, - child: Text(translate('Transfer file')), - ), - MenuItemButton( - onPressed: () { - onConnect(isViewCamera: true); - }, - child: Text(translate('View camera')), - ), - ], - ), + child: Obx(() { + var offset = Offset(0, 0); + return InkWell( + child: _menuOpen.value + ? Transform.rotate( + angle: pi, + child: Icon(IconFont.more, size: 14), + ) + : Icon(IconFont.more, size: 14), + onTapDown: (e) { + offset = e.globalPosition; + }, + onTap: () async { + _menuOpen.value = true; + final x = offset.dx; + final y = offset.dy; + await mod_menu + .showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + items: [ + ( + 'Transfer file', + () => onConnect(isFileTransfer: true) + ), + ( + 'View camera', + () => onConnect(isViewCamera: true) + ), + ] + .map((e) => MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate(e.$1), + style: style, + ), + proc: () => e.$2(), + padding: EdgeInsets.symmetric( + horizontal: kDesktopMenuPadding.left), + dismissOnClicked: true, + )) + .map((e) => e.build( + context, + const MenuConfig( + commonColor: + CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme + .dividerHeight))) + .expand((i) => i) + .toList(), + elevation: 8, + ) + .then((_) { + _menuOpen.value = false; + }); + }, + ); + }), ), ), ]), diff --git a/flutter/lib/desktop/pages/view_camera_page.dart b/flutter/lib/desktop/pages/view_camera_page.dart index b06dc86c563..d8611401441 100644 --- a/flutter/lib/desktop/pages/view_camera_page.dart +++ b/flutter/lib/desktop/pages/view_camera_page.dart @@ -229,7 +229,7 @@ class _ViewCameraPageState extends State // https://github.com/flutter/flutter/issues/64935 super.dispose(); - debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); + debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}"); _ffi.textureModel.onViewCameraPageDispose(closeSession); if (closeSession) { // ensure we leave this session, this is a double check diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a9aae014eff..d16365fa554 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -164,6 +164,9 @@ flutter: - family: DeviceGroup fonts: - asset: assets/device_group.ttf + - family: More + fonts: + - asset: assets/more.ttf # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. From 8f7bb5a0324d853627bfb38282c3e3835663f6a1 Mon Sep 17 00:00:00 2001 From: XLion Date: Wed, 12 Mar 2025 15:14:21 +0800 Subject: [PATCH 142/506] Update tw.rs (#11087) --- src/lang/tw.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 43b0e724fab..b41f6c78779 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -658,9 +658,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "有新版本的 {} 可用"), ("Accessible devices", "可存取的裝置"), ("View camera", "檢視相機"), - ("upgrade_remote_rustdesk_client_to_{}_tip", "請將遠端 RustDesk 用戶端升級到 {} 或更新版本!"), - ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "請將遠端 RustDesk 客戶端升級到 {} 或更新版本!"), + ("view_camera_unsupported_tip", "您的遠端設備不支援查看鏡頭"), + ("Enable camera", "允許查看鏡頭"), + ("No cameras", "沒有鏡頭"), ].iter().cloned().collect(); } From 22005bac759b80ffa7d992128c170b27c883a875 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Wed, 12 Mar 2025 08:14:47 +0100 Subject: [PATCH 143/506] Italian language update (#11088) --- 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 80dbee3ca0c..f3bf7b11fd1 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -659,8 +659,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", "Dispositivi accessibili"), ("View camera", "Visualizza telecamera"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Aggiorna il client RustDesk remoto alla versione {} o successiva!"), - ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), + ("view_camera_unsupported_tip", "Il dispositivo remoto non supporta la visualizzazione della camera."), + ("Enable camera", "Abilita camera"), + ("No cameras", "Nessuna camera"), ].iter().cloned().collect(); } From f7c930e15339a8c3fe3017d248e03c1d0ce676df Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 12 Mar 2025 19:07:08 +0800 Subject: [PATCH 144/506] fix https://github.com/rustdesk/rustdesk/discussions/11104 --- libs/hbb_common | 2 +- src/platform/privileges_scripts/agent.plist | 2 +- src/platform/privileges_scripts/daemon.plist | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/hbb_common b/libs/hbb_common index 83419b65496..7cf11f7b771 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 83419b6549636ee39dacef7776c473f5802e08d6 +Subproject commit 7cf11f7b771e27ecbd14fd1dd0ced55a64f40eb5 diff --git a/src/platform/privileges_scripts/agent.plist b/src/platform/privileges_scripts/agent.plist index 5ff786a4932..398e1d12607 100644 --- a/src/platform/privileges_scripts/agent.plist +++ b/src/platform/privileges_scripts/agent.plist @@ -3,7 +3,7 @@ Label - com.carriez.RustDesk_server + com.carriez.rustdesk.server LimitLoadToSessionType LoginWindow diff --git a/src/platform/privileges_scripts/daemon.plist b/src/platform/privileges_scripts/daemon.plist index 59f103a3138..d3065b93530 100644 --- a/src/platform/privileges_scripts/daemon.plist +++ b/src/platform/privileges_scripts/daemon.plist @@ -3,7 +3,7 @@ Label - com.carriez.RustDesk_service + com.carriez.rustdesk.service KeepAlive ThrottleInterval From f1d2073d43ec28e6baa6d3c0324cd07b9a8198bb Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 12 Mar 2025 20:06:35 +0800 Subject: [PATCH 145/506] revert back, because useless --- src/platform/privileges_scripts/agent.plist | 2 +- src/platform/privileges_scripts/daemon.plist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/privileges_scripts/agent.plist b/src/platform/privileges_scripts/agent.plist index 398e1d12607..5ff786a4932 100644 --- a/src/platform/privileges_scripts/agent.plist +++ b/src/platform/privileges_scripts/agent.plist @@ -3,7 +3,7 @@ Label - com.carriez.rustdesk.server + com.carriez.RustDesk_server LimitLoadToSessionType LoginWindow diff --git a/src/platform/privileges_scripts/daemon.plist b/src/platform/privileges_scripts/daemon.plist index d3065b93530..59f103a3138 100644 --- a/src/platform/privileges_scripts/daemon.plist +++ b/src/platform/privileges_scripts/daemon.plist @@ -3,7 +3,7 @@ Label - com.carriez.rustdesk.service + com.carriez.RustDesk_service KeepAlive ThrottleInterval From 1403c939db48dad649b2341e44dfd5fb168e6682 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 13 Mar 2025 09:33:30 +0800 Subject: [PATCH 146/506] fix: msi, silent install, do not launch app (#11115) Signed-off-by: fufesou --- res/msi/Package/Components/RustDesk.wxs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index c17d60edb79..ff39f844344 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -53,8 +53,9 @@ - - + + + From d1c8b331c59e60d4ab8eb1e285cb7163c3dba235 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 13 Mar 2025 09:34:13 +0800 Subject: [PATCH 147/506] Option `allow-d3d-render` and fix ios ci (#11107) * option `allow-d3d-render`, default false Add this option because it fails on some machines Signed-off-by: 21pages * only add nokhwa to windows and linux dependencies Signed-off-by: 21pages --------- Signed-off-by: 21pages --- flutter/lib/consts.dart | 1 + .../desktop/pages/desktop_setting_page.dart | 10 ++++ libs/hbb_common | 2 +- libs/scrap/Cargo.toml | 4 +- libs/scrap/src/common/camera.rs | 59 +++++++++++++++---- libs/scrap/src/common/codec.rs | 13 ++-- libs/scrap/src/common/mod.rs | 3 +- libs/scrap/src/common/record.rs | 9 ++- src/client.rs | 18 ++---- src/flutter_ffi.rs | 8 ++- 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 | 1 + 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/sc.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/ta.rs | 7 +++ 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/server/video_service.rs | 14 ++++- src/ui/header.tis | 4 +- src/ui/remote.rs | 2 +- src/ui_session_interface.rs | 4 +- 60 files changed, 204 insertions(+), 43 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index d5b133bf198..011709cc598 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -78,6 +78,7 @@ const String kOptionScrollStyle = "scroll_style"; const String kOptionImageQuality = "image_quality"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionTextureRender = "use-texture-render"; +const String kOptionD3DRender = "allow-d3d-render"; const String kOptionOpenInTabs = "allow-open-in-tabs"; const String kOptionOpenInWindows = "allow-open-in-windows"; const String kOptionForceAlwaysRelay = "force-always-relay"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index ad9a1296ca6..5d95c8d0cf6 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -496,6 +496,16 @@ class _GeneralState extends State<_General> { await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'), ), ), + if (isWindows) + Tooltip( + message: translate('d3d_render_tip'), + child: _OptionCheckBox( + context, + "Use D3D rendering", + kOptionD3DRender, + isServer: false, + ), + ), if (!isWeb && !bind.isCustomClient()) _OptionCheckBox( context, diff --git a/libs/hbb_common b/libs/hbb_common index 7cf11f7b771..1819875476d 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 7cf11f7b771e27ecbd14fd1dd0ced55a64f40eb5 +Subproject commit 1819875476d487612a654099881b8a16f4337599 diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index d336f2d3b1e..16196d11f25 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -23,7 +23,6 @@ lazy_static = "1.4" hbb_common = { path = "../hbb_common" } webm = { git = "https://github.com/rustdesk-org/rust-webm" } serde = {version="1.0", features=["derive"]} -nokhwa = { git = "https://github.com/rustdesk-org/nokhwa.git", branch = "fix_from_raw_parts", features = ["input-native"] } [dependencies.winapi] version = "0.3" @@ -63,3 +62,6 @@ gstreamer-video = { version = "0.16", optional = true } git = "https://github.com/rustdesk-org/hwcodec" optional = true +[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] +nokhwa = { git = "https://github.com/rustdesk-org/nokhwa.git", branch = "fix_from_raw_parts", features = ["input-native"] } + diff --git a/libs/scrap/src/common/camera.rs b/libs/scrap/src/common/camera.rs index e857423ed80..a84e8d7cd65 100644 --- a/libs/scrap/src/common/camera.rs +++ b/libs/scrap/src/common/camera.rs @@ -3,6 +3,7 @@ use std::{ sync::{Arc, Mutex}, }; +#[cfg(any(target_os = "windows", target_os = "linux"))] use nokhwa::{ pixel_format::RgbAFormat, query, @@ -23,6 +24,9 @@ lazy_static::lazy_static! { static ref SYNC_CAMERA_DISPLAYS: Arc>> = Arc::new(Mutex::new(Vec::new())); } +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +const CAMERA_NOT_SUPPORTED: &str = "This platform doesn't support camera yet"; + pub struct Cameras; // pre-condition @@ -30,12 +34,9 @@ pub fn primary_camera_exists() -> bool { Cameras::exists(PRIMARY_CAMERA_IDX) } +#[cfg(any(target_os = "windows", target_os = "linux"))] impl Cameras { pub fn all_info() -> ResultType> { - // TODO: support more platforms. - #[cfg(not(any(target_os = "linux", target_os = "windows")))] - return Ok(Vec::new()); - match query(ApiBackend::Auto) { Ok(cameras) => { let mut camera_displays = SYNC_CAMERA_DISPLAYS.lock().unwrap(); @@ -102,10 +103,6 @@ impl Cameras { } pub fn exists(index: usize) -> bool { - // TODO: support more platforms. - #[cfg(not(any(target_os = "linux", target_os = "windows")))] - return false; - match query(ApiBackend::Auto) { Ok(cameras) => index < cameras.len(), _ => return false, @@ -113,10 +110,6 @@ impl Cameras { } fn create_camera(index: &CameraIndex) -> ResultType { - // TODO: support more platforms. - #[cfg(not(any(target_os = "linux", target_os = "windows")))] - bail!("This platform doesn't support camera yet"); - let result = Camera::new( index.clone(), RequestedFormat::new::(RequestedFormatType::AbsoluteHighestResolution), @@ -147,13 +140,41 @@ impl Cameras { } } +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +impl Cameras { + pub fn all_info() -> ResultType> { + return Ok(Vec::new()); + } + + pub fn exists(index: usize) -> bool { + false + } + + pub fn get_camera_resolution(index: usize) -> ResultType { + bail!(CAMERA_NOT_SUPPORTED); + } + + pub fn get_sync_cameras() -> Vec { + vec![] + } + + pub fn get_capturer(current: usize) -> ResultType> { + bail!(CAMERA_NOT_SUPPORTED); + } +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] pub struct CameraCapturer { camera: Camera, data: Vec, last_data: Vec, // for faster compare and copy } +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +pub struct CameraCapturer; + impl CameraCapturer { + #[cfg(any(target_os = "windows", target_os = "linux"))] fn new(current: usize) -> ResultType { let index = CameraIndex::Index(current as u32); let camera = Cameras::create_camera(&index)?; @@ -163,9 +184,15 @@ impl CameraCapturer { last_data: Vec::new(), }) } + + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + fn new(_current: usize) -> ResultType { + bail!(CAMERA_NOT_SUPPORTED); + } } impl TraitCapturer for CameraCapturer { + #[cfg(any(target_os = "windows", target_os = "linux"))] fn frame<'a>(&'a mut self, _timeout: std::time::Duration) -> std::io::Result> { // TODO: move this check outside `frame`. if !self.camera.is_stream_open() { @@ -212,6 +239,14 @@ impl TraitCapturer for CameraCapturer { } } + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + fn frame<'a>(&'a mut self, _timeout: std::time::Duration) -> std::io::Result> { + Err(io::Error::new( + io::ErrorKind::Other, + CAMERA_NOT_SUPPORTED.to_string(), + )) + } + #[cfg(windows)] fn is_gdi(&self) -> bool { false diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 662ac02a542..8eb0c158978 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -864,7 +864,7 @@ pub fn enable_vram_option(encode: bool) -> bool { if encode { enable && enable_directx_capture() } else { - enable + enable && allow_d3d_render() } } else { false @@ -874,10 +874,13 @@ pub fn enable_vram_option(encode: bool) -> bool { #[cfg(windows)] pub fn enable_directx_capture() -> bool { use hbb_common::config::keys::OPTION_ENABLE_DIRECTX_CAPTURE as OPTION; - option2bool( - OPTION, - &Config::get_option(hbb_common::config::keys::OPTION_ENABLE_DIRECTX_CAPTURE), - ) + option2bool(OPTION, &Config::get_option(OPTION)) +} + +#[cfg(windows)] +pub fn allow_d3d_render() -> bool { + use hbb_common::config::keys::OPTION_ALLOW_D3D_RENDER as OPTION; + option2bool(OPTION, &hbb_common::config::LocalConfig::get_option(OPTION)) } pub const BR_BEST: f32 = 1.5; diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 7b8388fbd5b..af542197340 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -48,8 +48,9 @@ pub use self::convert::*; pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer -pub mod camera; pub mod aom; +#[cfg(not(any(target_os = "ios")))] +pub mod camera; pub mod record; mod vpx; diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index 628d2d5a3f7..d121984f1be 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -25,7 +25,8 @@ pub struct RecorderContext { pub server: bool, pub id: String, pub dir: String, - pub video_service_name: String, + pub display_idx: usize, + pub camera: bool, pub tx: Option>, } @@ -46,7 +47,11 @@ impl RecorderContext2 { + "_" + &ctx.id.clone() + &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string() - + &format!("{}_", ctx.video_service_name) + + &format!( + "{}{}_", + if ctx.camera { "camera" } else { "display" }, + ctx.display_idx + ) + &self.format.to_string().to_lowercase() + if self.format == CodecFormat::VP9 || self.format == CodecFormat::VP8 diff --git a/src/client.rs b/src/client.rs index 380559bb803..c056a64f3bc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1389,14 +1389,15 @@ impl VideoHandler { } /// Start or stop screen record. - pub fn record_screen(&mut self, start: bool, id: String, video_service_name: String) { + pub fn record_screen(&mut self, start: bool, id: String, display_idx: usize, camera: bool) { self.record = false; if start { self.recorder = Recorder::new(RecorderContext { server: false, id, dir: crate::ui_interface::video_save_directory(false), - video_service_name, + display_idx, + camera, tx: None, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); @@ -2437,14 +2438,7 @@ pub fn start_video_thread( { let mut video_callback = video_callback; let mut last_chroma = None; - let video_service_name = crate::video_service::get_service_name( - if session.is_view_camera() { - crate::video_service::VideoSource::Camera - } else { - crate::video_service::VideoSource::Monitor - }, - display, - ); + let is_view_camera = session.is_view_camera(); std::thread::spawn(move || { #[cfg(windows)] @@ -2487,7 +2481,7 @@ pub fn start_video_thread( let record_permission = session.lc.read().unwrap().record_permission; let id = session.lc.read().unwrap().id.clone(); if record_state && record_permission { - handler.record_screen(true, id, video_service_name.clone()); + handler.record_screen(true, id, display, is_view_camera); } video_handler = Some(handler); } @@ -2568,7 +2562,7 @@ pub fn start_video_thread( MediaData::RecordScreen(start) => { let id = session.lc.read().unwrap().id.clone(); if let Some(handler) = video_handler.as_mut() { - handler.record_screen(start, id, video_service_name.clone()); + handler.record_screen(start, id, display, is_view_camera); } } _ => {} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index c36b5844dad..a8d51990be0 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -993,6 +993,7 @@ pub fn main_get_env(key: String) -> SyncReturn { pub fn main_set_local_option(key: String, value: String) { let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER); + let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER); set_local_option(key, value.clone()); if is_texture_render_key { let session_event = [("v", &value)]; @@ -1002,6 +1003,11 @@ pub fn main_set_local_option(key: String, value: String) { session.ui_handler.update_use_texture_render(); } } + if is_d3d_render_key { + for session in sessions::get_sessions() { + session.update_supported_decodings(); + } + } } // We do use use `main_get_local_option` and `main_set_local_option`. @@ -1650,7 +1656,7 @@ pub fn session_alternative_codecs(session_id: SessionID) -> String { pub fn session_change_prefer_codec(session_id: SessionID) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.change_prefer_codec(); + session.update_supported_decodings(); } } diff --git a/src/lang/ar.rs b/src/lang/ar.rs index c96870b1553..0cd8b3e6c42 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 039bc2e2519..5c490713032 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 3b6e3171ec3..b153b035885 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 746b32e71c3..7a524f21c9e 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index bef8122a600..dd8d6d10ab9 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", "您的远程端不支持查看摄像头。"), ("Enable camera", "允许查看摄像头"), ("No cameras", "没有摄像头"), + ("d3d_render_tip", "当启用 D3D 渲染时,某些机器可能无法显示远程画面。"), + ("Use D3D rendering", "使用 D3D 渲染"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 941a8f035a4..f64a5df2386 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 177c040ace9..3f8f14cfb1c 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 2589a826e29..79c04d0a2ad 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 13c0889c31e..505a211c8dc 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 3b20a5bf298..26754a0a58b 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -240,5 +240,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "View camera"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Please upgrade the RustDesk client to version {} or newer on the remote side!"), ("view_camera_unsupported_tip", "The remote device does not support viewing the camera."), + ("d3d_render_tip", "When D3D rendering is enabled, the remote control screen may be black on some machines."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 903dad827f3..22a6c73f54c 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 0c56f95ccfc..989fda64b04 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index c0ccef343d3..f4db11cc355 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 54939966488..5d47f36c7f7 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 5db84e9e6c6..9fc72dc753e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 456dd1d5ca3..0f01fcd73ae 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index acbeae98023..b602d91ffc4 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index b52dbb4dba3..d9ff9b481aa 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 02e53c3a642..1ea2152b522 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 8dc82ff25e8..f6fac705317 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index f3bf7b11fd1..ef5b4b26061 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", "Il dispositivo remoto non supporta la visualizzazione della camera."), ("Enable camera", "Abilita camera"), ("No cameras", "Nessuna camera"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5daa909c550..bdd9b41b410 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 2929fa28cb2..08e27f8af93 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index ff5d756c494..b820e2f39d3 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 9929508c96e..97b4a205c32 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index fe3dce7e82d..48e3c2a7e86 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 9fb020f4f72..73dae60284a 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 68b126c5c24..a53bd30752b 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index e5459081831..37e6344670b 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e5ef2ca0f81..6246bce81f3 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 71c8a480397..e7c0ee5d1ab 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index ae5283e5b4b..95040f10961 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index b79dffe563d..310a35ccf56 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 4e5e8c76f0b..5ac42339c9a 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index eb8f53d81cf..531ccbf71f2 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index e4061caed68..c3fb9ec4f51 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 5649130fbd1..3142b231751 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index f4ba4048fea..90007d7eb30 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 3809373aa21..4493f7ab415 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 2cf3f1c7030..87ced6bf7d5 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -657,5 +657,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), + ("View camera", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("view_camera_unsupported_tip", ""), + ("Enable camera", ""), + ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index cc9197b5d47..ccf3b9bcd79 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 7674d059da6..c1b41d2df1c 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f7ef7d3279f..e248898f9e7 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index b41f6c78779..60a3696c9c3 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", "您的遠端設備不支援查看鏡頭"), ("Enable camera", "允許查看鏡頭"), ("No cameras", "沒有鏡頭"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index f1f7e76f49e..578e889e511 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index d1254960d65..61a63f2afc6 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -662,5 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", ""), ("Enable camera", ""), ("No cameras", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), ].iter().cloned().collect(); } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 04f63fe75b4..50831653553 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -503,6 +503,7 @@ fn run(vs: VideoService) -> ResultType<()> { record_incoming, last_portable_service_running, vs.source, + display_idx, ) { Ok(result) => result, Err(err) => { @@ -522,6 +523,7 @@ fn run(vs: VideoService) -> ResultType<()> { record_incoming, last_portable_service_running, vs.source, + display_idx, )? } }; @@ -791,6 +793,7 @@ fn setup_encoder( record_incoming: bool, last_portable_service_running: bool, source: VideoSource, + display_idx: usize, ) -> ResultType<( Encoder, EncoderCfg, @@ -808,7 +811,7 @@ fn setup_encoder( ); Encoder::set_fallback(&encoder_cfg); let codec_format = Encoder::negotiated_codec(); - let recorder = get_recorder(record_incoming, name); + let recorder = get_recorder(record_incoming, display_idx, source == VideoSource::Camera); 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)) @@ -891,7 +894,11 @@ fn get_encoder_config( } } -fn get_recorder(record_incoming: bool, video_service_name: String) -> Arc>> { +fn get_recorder( + record_incoming: bool, + display_idx: usize, + camera: bool, +) -> Arc>> { #[cfg(windows)] let root = crate::platform::is_root(); #[cfg(not(windows))] @@ -910,7 +917,8 @@ fn get_recorder(record_incoming: bool, video_service_name: String) -> Arc Session { (vp8, av1, h264, h265) } - pub fn change_prefer_codec(&self) { + pub fn update_supported_decodings(&self) { let msg = self.lc.write().unwrap().update_supported_decodings(); self.send(Data::Message(msg)); } pub fn use_texture_render_changed(&self) { self.send(Data::ResetDecoder(None)); - self.change_prefer_codec(); + self.update_supported_decodings(); self.send(Data::Message(LoginConfigHandler::refresh())); } From 47b00054d27b08091dce940cb3ba9944bddaa0c1 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Thu, 13 Mar 2025 02:35:21 +0100 Subject: [PATCH 148/506] Update nl.rs (#11102) Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- 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 a53bd30752b..4e8a29e737b 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -659,9 +659,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", "Toegankelijke apparaten"), ("View camera", "Camera bekijken"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgrade de RustDesk client naar versie {} of nieuwer op de externe computer!"), - ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), + ("view_camera_unsupported_tip", "Het externe apparaat ondersteunt geen cameraweergave."), + ("Enable camera", "Camera inschakelen"), + ("No cameras", "Geen camera's"), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ].iter().cloned().collect(); From 2bdb62141757f6ee4a35816e02296011c1a10154 Mon Sep 17 00:00:00 2001 From: flusheDData <116861809+flusheDData@users.noreply.github.com> Date: Thu, 13 Mar 2025 02:36:00 +0100 Subject: [PATCH 149/506] Update es.rs (#11111) Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- 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 989fda64b04..d889dba740b 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -659,9 +659,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", ""), ("View camera", "Ver cámara"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"), - ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), + ("view_camera_unsupported_tip", "El dispositivo remoto no soporta la visualización de la cámara."), + ("Enable camera", "Habilitar cámara"), + ("No cameras", "No hay cámaras"), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ].iter().cloned().collect(); From 2dbff45588a3b62eae8516d78ed69f80f33fb9e4 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Thu, 13 Mar 2025 02:36:27 +0100 Subject: [PATCH 150/506] Update de.rs (#11106) Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- 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 79c04d0a2ad..f9f8994ed41 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -659,9 +659,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", "Erreichbare Geräte"), ("View camera", "Kamera anzeigen"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Bitte aktualisieren Sie den RustDesk-Client auf der Remote-Seite auf Version {} oder neuer!"), - ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), + ("view_camera_unsupported_tip", "Das entfernte Gerät kann die Kamera nicht anzeigen."), + ("Enable camera", "Kamera zulassen"), + ("No cameras", "Keine Kameras"), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ].iter().cloned().collect(); From 971d4e6976380ed7d3ec20903ca29a48bd530887 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 14 Mar 2025 00:21:05 +0800 Subject: [PATCH 151/506] ipc example for test (#11127) Signed-off-by: 21pages --- Cargo.lock | 1 + Cargo.toml | 1 + examples/ipc.rs | 90 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 examples/ipc.rs diff --git a/Cargo.lock b/Cargo.lock index 621d8d35826..f4f61833dea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5772,6 +5772,7 @@ dependencies = [ "dbus-crossroads", "default-net", "dispatch", + "docopt", "enigo", "errno", "evdev", diff --git a/Cargo.toml b/Cargo.toml index 6de64f5e73b..317ab123c55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -197,6 +197,7 @@ os-version = "0.2" [dev-dependencies] hound = "3.5" +docopt = "1.1" [package.metadata.bundle] name = "RustDesk" diff --git a/examples/ipc.rs b/examples/ipc.rs new file mode 100644 index 00000000000..bca2321b124 --- /dev/null +++ b/examples/ipc.rs @@ -0,0 +1,90 @@ +use docopt::Docopt; +use hbb_common::{ + env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV}, + log, tokio, +}; +use librustdesk::{ipc::Data, *}; + +const USAGE: &'static str = " +IPC test program. + +Usage: + ipc (-s | --server | -c | --client) [-p | --postfix=] + ipc (-h | --help) + +Options: + -h --help Show this screen. + -s --server Run as IPC server. + -c --client Run as IPC client. + -p --postfix= IPC path postfix [default: ]. +"; + +#[derive(Debug, serde::Deserialize)] +struct Args { + flag_server: bool, + flag_client: bool, + flag_postfix: String, +} + +#[tokio::main] +async fn main() { + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); + + let args: Args = Docopt::new(USAGE) + .and_then(|d| d.deserialize()) + .unwrap_or_else(|e| e.exit()); + + if args.flag_server { + if args.flag_postfix.is_empty() { + log::info!("Starting IPC server..."); + } else { + log::info!( + "Starting IPC server with postfix: '{}'...", + args.flag_postfix + ); + } + ipc_server(&args.flag_postfix).await; + } else if args.flag_client { + if args.flag_postfix.is_empty() { + log::info!("Starting IPC client..."); + } else { + log::info!( + "Starting IPC client with postfix: '{}'...", + args.flag_postfix + ); + } + ipc_client(&args.flag_postfix).await; + } +} + +async fn ipc_server(postfix: &str) { + let postfix = postfix.to_string(); + let postfix2 = postfix.clone(); + std::thread::spawn(move || { + if let Err(err) = crate::ipc::start(&postfix) { + log::error!("Failed to start ipc: {}", err); + std::process::exit(-1); + } + }); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + ipc_client(&postfix2).await; +} + +async fn ipc_client(postfix: &str) { + loop { + match crate::ipc::connect(1000, postfix).await { + Ok(mut conn) => match conn.send(&Data::Empty).await { + Ok(_) => { + log::info!("send message to ipc server success"); + } + Err(e) => { + log::error!("Failed to send message to ipc server: {}", e); + } + }, + Err(e) => { + log::error!("Failed to connect to ipc server: {}", e); + } + } + tokio::time::sleep(std::time::Duration::from_secs(6)).await; + } +} From 10457dfe450cece8bca6116b13997c5632abf6ce Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Fri, 14 Mar 2025 10:14:05 +0100 Subject: [PATCH 152/506] Italian language update (#11123) --- 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 ef5b4b26061..58fda3f7ef3 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -662,7 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", "Il dispositivo remoto non supporta la visualizzazione della camera."), ("Enable camera", "Abilita camera"), ("No cameras", "Nessuna camera"), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), + ("d3d_render_tip", "Quando è abilitato il rendering D3D, in alcuni computer la schermata del telecomando potrebbe essere nera."), + ("Use D3D rendering", "Usa rendering D3D"), ].iter().cloned().collect(); } From 960d9a042f2102f5970611bff8109eae66414dcf Mon Sep 17 00:00:00 2001 From: XLion Date: Sat, 15 Mar 2025 21:37:39 +0800 Subject: [PATCH 153/506] Update tw.rs (#11126) --- 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 60a3696c9c3..4a2647bb166 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -662,7 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", "您的遠端設備不支援查看鏡頭"), ("Enable camera", "允許查看鏡頭"), ("No cameras", "沒有鏡頭"), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), + ("d3d_render_tip", "當啟用 D3D 渲染時,某些機器可能會無法顯示遠端畫面。"), + ("Use D3D rendering", "使用 D3D 渲染"), ].iter().cloned().collect(); } From 2ef1dd99de6c56054c4141fd722db7e9c93e65a9 Mon Sep 17 00:00:00 2001 From: solokot Date: Sun, 16 Mar 2025 10:11:18 +0300 Subject: [PATCH 154/506] Update ru.rs (#11132) --- 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 310a35ccf56..6c0698d0ab0 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -567,8 +567,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("id_input_tip", "Можно ввести идентификатор, прямой 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"), @@ -658,11 +658,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Доступна новая версия {}"), ("Accessible devices", "Доступные устройства"), ("View camera", "Просмотр камеры"), - ("upgrade_remote_rustdesk_client_to_{}_tip", "Обновите клиент RustDesk до версии {} или новее на удаленной стороне!"), - ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Обновите клиент RustDesk до версии {} или новее на удалённой стороне!"), + ("view_camera_unsupported_tip", "Удалённое устройство не поддерживает просмотр камеры."), + ("Enable camera", "Включить камеру"), + ("No cameras", "Камера отсутствует"), + ("d3d_render_tip", "При включении визуализации D3D на некоторых устройствах удалённый экран может быть чёрным."), + ("Use D3D rendering", "Использовать визуализацию D3D"), ].iter().cloned().collect(); } From 745ba1673d3f943559dbf7c8968ed8193f021575 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Mon, 17 Mar 2025 15:04:18 +0100 Subject: [PATCH 155/506] Update nl.rs (#11155) --- 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 4e8a29e737b..7bfd50d54b2 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -662,7 +662,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", "Het externe apparaat ondersteunt geen cameraweergave."), ("Enable camera", "Camera inschakelen"), ("No cameras", "Geen camera's"), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), + ("d3d_render_tip", "Wanneer D3D-rendering is ingeschakeld kan het externe scherm op sommige apparaten, zwart zijn."), + ("Use D3D rendering", "Gebruik D3D-rendering"), ].iter().cloned().collect(); } From 8d231b46050d45fed53fa239bb3b07f5705f7dcf Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Tue, 18 Mar 2025 07:48:48 +0100 Subject: [PATCH 156/506] Update nl.rs (#11172) From c06ac9341a9918bfeab416302903094b2dae7cd8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 18 Mar 2025 15:12:41 +0800 Subject: [PATCH 157/506] improve check_software_update --- src/common.rs | 8 +++++++- src/flutter_ffi.rs | 5 +---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/common.rs b/src/common.rs index 23f7b4b0f0c..83272a71071 100644 --- a/src/common.rs +++ b/src/common.rs @@ -826,7 +826,13 @@ pub fn is_modifier(evt: &KeyEvent) -> bool { } pub fn check_software_update() { - std::thread::spawn(move || allow_err!(check_software_update_())); + if is_custom_client() { + return; + } + let opt = config::LocalConfig::get_option(config::keys::OPTION_ENABLE_CHECK_UPDATE); + if config::option2bool(config::keys::OPTION_ENABLE_CHECK_UPDATE, &opt) { + std::thread::spawn(move || allow_err!(check_software_update_())); + } } #[tokio::main(flavor = "current_thread")] diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a8d51990be0..2e66a11a2bf 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1446,10 +1446,7 @@ pub fn main_get_last_remote_id() -> String { } pub fn main_get_software_update_url() { - 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(); - } + crate::common::check_software_update(); } pub fn main_get_home_dir() -> String { From 47c93f8544b98f6ee37a0fa80edfde8e8ab77cb2 Mon Sep 17 00:00:00 2001 From: asereze Date: Wed, 19 Mar 2025 15:26:14 +0100 Subject: [PATCH 158/506] Update sc.rs (#11176) * Update sc.rs * Update sc.rs --- src/lang/sc.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 5ac42339c9a..f5f57d3d14f 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -657,12 +657,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Chene tag"), ("new-version-of-{}-tip", "B'at una versione noa de {} a disponimentu"), ("Accessible devices", "Dispositivos atzessìbiles"), - ("View camera", "Mustra càmera"), - ("upgrade_remote_rustdesk_client_to_{}_tip", "¡Actualice el cliente RustDesk a la versión {} o más reciente en el lado remoto!"), - ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), + ("View camera", "Mustra sa càmera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualiza su cliente RustDesk remotu a sa versione {} o prus noa!"), + ("view_camera_unsupported_tip", "Su dispositivu remotu non suportat sa visualizatzione de sa càmera"), + ("Enable camera", "Abìlita sa càmera"), + ("No cameras", "Peruna càmera"), + ("d3d_render_tip", "Cando sa renderizatzione D3D est abilitada, s'ischermu de controllu remotu diat pòdere èssere nieddu in unas cantas màchinas"), + ("Use D3D rendering", "Imprea sa renderizatzione D3D"), ].iter().cloned().collect(); } From c074a1d6af9254b4350fd42fc514b92592cf1a99 Mon Sep 17 00:00:00 2001 From: Lynilia <89228568+Lynilia@users.noreply.github.com> Date: Thu, 20 Mar 2025 08:12:03 +0100 Subject: [PATCH 159/506] Update fr.rs (#11184) --- src/lang/fr.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 0f01fcd73ae..6c5761437ea 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -657,12 +657,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sans étiquette"), ("new-version-of-{}-tip", "Une nouvelle version de {} est disponible"), ("Accessible devices", "Appareils accessibles"), - ("View camera", "Voir la caméra"), - ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre à jour le client RustDesk avec la version {} ou une version plus récente sur l'appareil distant"), - ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), + ("View camera", "Afficher la caméra"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre le client RustDesk distant à jour vers la version {} ou ultérieure !"), + ("view_camera_unsupported_tip", "L’appareil distant ne prend pas en charge l’affichage de la caméra."), + ("Enable camera", "Activer la caméra"), + ("No cameras", "Aucune caméra"), + ("d3d_render_tip", "Sur certaines machines, l’écran du contrôle à distance peut rester noir lors de l’utilisation du rendu D3D."), + ("Use D3D rendering", "Utiliser le rendu D3D"), ].iter().cloned().collect(); } From 9831f93430c7ad917df2216fab0dff945431ad3f Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Mon, 24 Mar 2025 21:59:08 +0800 Subject: [PATCH 160/506] Update README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 021338d6db0..01bc0c33261 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,11 @@ We need your help to translate this README, RustDesk UI and RustDesk Doc to your native language

    +> [!Caution] +> **Misuse Disclaimer:**
    +> The developers of RustDesk do not condone or support any unethical or illegal use of this software. Misuse, such as unauthorized access, control or invasion of privacy, is strictly against our guidelines. The authors are not responsible for any misuse of the application. + + Chat with us: [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) @@ -165,10 +170,6 @@ Please ensure that you are running these commands from the root of the RustDesk - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript for Flutter web client -> [!Caution] -> **Misuse Disclaimer:**
    -> The developers of RustDesk do not condone or support any unethical or illegal use of this software. Misuse, such as unauthorized access, control or invasion of privacy, is strictly against our guidelines. The authors are not responsible for any misuse of the application. - ## Screenshots ![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) From ac630c2ca63530bf63344ecde759351b609c2404 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 25 Mar 2025 15:41:36 +0800 Subject: [PATCH 161/506] Update README-ZH.md --- docs/README-ZH.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/README-ZH.md b/docs/README-ZH.md index dc4543f7893..83702f8f3f6 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -8,6 +8,10 @@ [English] | [Українська] | [česky] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]

    +> [!警告] +> **免责声明:**
    +> 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) @@ -218,10 +222,6 @@ target/release/rustdesk - **[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代码 -> [!警告] -> **免责声明:**
    -> RustDesk 的开发人员不纵容或支持任何不道德或非法的软件使用行为。滥用行为,例如未经授权的访问、控制或侵犯隐私,严格违反我们的准则。作者对应用程序的任何滥用行为概不负责。 - ## 截图 ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) From eea9e0fa431f05a4619e63b6bda80b6a1d699f38 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 26 Mar 2025 15:23:40 +0800 Subject: [PATCH 162/506] Update README-DE.md (#11247) --- docs/README-DE.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/README-DE.md b/docs/README-DE.md index 02144acdaaa..284981889b2 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -9,6 +9,11 @@ Wir brauchen Ihre Hilfe, um dieses README, die RustDesk-Benutzeroberfläche und die Dokumentation in Ihre Muttersprache zu übersetzen.

    +> [!Vorsicht] +> **Haftungsausschluss bei Missbrauch::**
    +> Die Entwickler von RustDesk billigen oder unterstützen keine unethische oder illegale Nutzung dieser Software. Missbrauch, wie unbefugter Zugriff, unbefugte Kontrolle oder Verletzung der Privatsphäre, verstößt strikt gegen unsere Richtlinien. Die Autoren sind nicht verantwortlich für jeglichen Missbrauch der Anwendung. + + Reden Sie mit uns auf: [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) @@ -147,10 +152,6 @@ target/release/rustdesk Bitte stellen Sie sicher, dass Sie diese Befehle im Stammverzeichnis des RustDesk-Repositorys nutzen. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenken Sie auch, dass andere Cargo-Unterbefehle wie `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf Ihrem eigentlichen System. -> [!Vorsicht] -> **Haftungsausschluss bei Missbrauch::**
    -> Die Entwickler von RustDesk billigen oder unterstützen keine unethische oder illegale Nutzung dieser Software. Missbrauch, wie unbefugter Zugriff, unbefugte Kontrolle oder Verletzung der Privatsphäre, verstößt strikt gegen unsere Richtlinien. Die Autoren sind nicht verantwortlich für jeglichen Missbrauch der Anwendung. - ## Dateistruktur - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video-Codec, Konfiguration, TCP/UDP-Wrapper, Protokoll-Puffer, fs-Funktionen für Dateitransfer und ein paar andere nützliche Funktionen From 1cb53c1f7a44c2f2f8839be5359339d610b5dbea Mon Sep 17 00:00:00 2001 From: Kleofass <4000163+Kleofass@users.noreply.github.com> Date: Thu, 27 Mar 2025 08:42:31 +0200 Subject: [PATCH 163/506] Update lv.rs (#11250) --- 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 48e3c2a7e86..45942447d0d 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -659,10 +659,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", "Pieejamas ierīces"), ("View camera", "Skatīt kameru"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Lūdzu, jauniniet attālās puses RustDesk klientu uz versiju {} vai jaunāku!"), - ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), + ("view_camera_unsupported_tip", "Attālā ierīce neatbalsta kameras skatīšanos."), + ("Enable camera", "Iespējot kameru"), + ("No cameras", "Nav kameru"), + ("d3d_render_tip", "Ja ir iespējota D3D renderēšana, dažās ierīcēs tālvadības pults ekrāns var būt melns."), + ("Use D3D rendering", "Izmantot D3D renderēšanu"), ].iter().cloned().collect(); } From f4bbf82363d78e35d0a61a0c2b82906822256867 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 27 Mar 2025 15:34:27 +0800 Subject: [PATCH 164/506] feat: remote printer (#11231) Signed-off-by: fufesou --- .github/workflows/flutter-build.yml | 34 +- Cargo.lock | 25 + Cargo.toml | 4 +- flutter/lib/common.dart | 26 + flutter/lib/common/widgets/dialog.dart | 1 - flutter/lib/consts.dart | 9 + .../desktop/pages/desktop_setting_page.dart | 161 ++++++ flutter/lib/desktop/pages/install_page.dart | 7 +- flutter/lib/models/file_model.dart | 11 +- flutter/lib/models/model.dart | 176 +++++- flutter/lib/models/native_model.dart | 4 +- flutter/lib/models/printer_model.dart | 48 ++ libs/hbb_common | 2 +- libs/remote_printer/Cargo.toml | 11 + libs/remote_printer/src/lib.rs | 34 ++ libs/remote_printer/src/setup/driver.rs | 202 +++++++ libs/remote_printer/src/setup/mod.rs | 99 ++++ libs/remote_printer/src/setup/port.rs | 128 +++++ libs/remote_printer/src/setup/printer.rs | 161 ++++++ libs/remote_printer/src/setup/setup.rs | 94 ++++ res/inline-sciter.py | 3 +- res/msi/CustomActions/Common.h | 6 + res/msi/CustomActions/CustomActions.cpp | 52 ++ res/msi/CustomActions/CustomActions.def | 2 + res/msi/CustomActions/CustomActions.vcxproj | 1 + res/msi/CustomActions/RemotePrinter.cpp | 517 ++++++++++++++++++ res/msi/Package/Components/RustDesk.wxs | 15 + res/msi/Package/Fragments/CustomActions.wxs | 2 + .../Package/Fragments/ShortcutProperties.wxs | 17 + res/msi/Package/Language/Package.en-us.wxl | 1 + res/msi/Package/Package.wxs | 2 + res/msi/Package/UI/MyInstallDirDlg.wxs | 1 + res/msi/preprocess.py | 14 + src/client.rs | 5 +- src/client/file_trait.rs | 12 + src/client/io_loop.rs | 288 ++++++---- src/common.rs | 4 + src/core_main.rs | 11 +- src/flutter.rs | 8 + src/flutter_ffi.rs | 93 +++- src/ipc.rs | 4 +- src/lang/ar.rs | 17 + src/lang/be.rs | 17 + src/lang/bg.rs | 17 + src/lang/ca.rs | 17 + src/lang/cn.rs | 17 + src/lang/cs.rs | 17 + src/lang/da.rs | 17 + src/lang/de.rs | 17 + src/lang/el.rs | 17 + src/lang/en.rs | 12 + src/lang/eo.rs | 17 + src/lang/es.rs | 17 + src/lang/et.rs | 17 + src/lang/eu.rs | 17 + src/lang/fa.rs | 17 + src/lang/fr.rs | 17 + src/lang/he.rs | 17 + src/lang/hr.rs | 17 + src/lang/hu.rs | 17 + src/lang/id.rs | 17 + src/lang/it.rs | 17 + src/lang/ja.rs | 17 + src/lang/ko.rs | 17 + src/lang/kz.rs | 17 + src/lang/lt.rs | 17 + src/lang/lv.rs | 17 + src/lang/nb.rs | 17 + src/lang/nl.rs | 17 + src/lang/pl.rs | 17 + src/lang/pt_PT.rs | 17 + src/lang/ptbr.rs | 17 + src/lang/ro.rs | 17 + src/lang/ru.rs | 17 + src/lang/sc.rs | 17 + src/lang/sk.rs | 17 + src/lang/sl.rs | 17 + src/lang/sq.rs | 17 + src/lang/sr.rs | 17 + src/lang/sv.rs | 17 + src/lang/ta.rs | 17 + src/lang/template.rs | 17 + src/lang/th.rs | 17 + src/lang/tr.rs | 17 + src/lang/tw.rs | 17 + src/lang/uk.rs | 17 + src/lang/vn.rs | 17 + src/platform/windows.cc | 115 +++- src/platform/windows.rs | 177 +++++- src/server.rs | 17 + src/server/connection.rs | 188 +++++-- src/server/input_service.rs | 2 +- src/server/portable_service.rs | 2 +- src/server/printer_service.rs | 163 ++++++ src/ui/file_transfer.tis | 23 +- src/ui/msgbox.tis | 3 + src/ui/printer.tis | 41 ++ src/ui/remote.html | 1 + src/ui/remote.rs | 28 +- src/ui_cm_interface.rs | 70 +-- src/ui_session_interface.rs | 16 + 101 files changed, 3707 insertions(+), 211 deletions(-) create mode 100644 flutter/lib/models/printer_model.dart create mode 100644 libs/remote_printer/Cargo.toml create mode 100644 libs/remote_printer/src/lib.rs create mode 100644 libs/remote_printer/src/setup/driver.rs create mode 100644 libs/remote_printer/src/setup/mod.rs create mode 100644 libs/remote_printer/src/setup/port.rs create mode 100644 libs/remote_printer/src/setup/printer.rs create mode 100644 libs/remote_printer/src/setup/setup.rs create mode 100644 res/msi/CustomActions/RemotePrinter.cpp create mode 100644 src/server/printer_service.rs create mode 100644 src/ui/printer.tis diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 825c67ec433..9d18f3bb1f9 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -159,14 +159,44 @@ jobs: - name: Build rustdesk run: | + # Windows: build RustDesk + python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack + mv ./flutter/build/windows/x64/runner/Release ./rustdesk + + # Download usbmmidd_v2.zip and extract it to ./rustdesk Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip Expand-Archive usbmmidd_v2.zip -DestinationPath . - python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack Remove-Item -Path usbmmidd_v2\Win32 -Recurse Remove-Item -Path "usbmmidd_v2\deviceinstaller64.exe", "usbmmidd_v2\deviceinstaller.exe", "usbmmidd_v2\usbmmidd.bat" - mv ./flutter/build/windows/x64/runner/Release ./rustdesk mv -Force .\usbmmidd_v2 ./rustdesk + # Download printer driver files and extract them to ./rustdesk + try { + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4.zip -OutFile rustdesk_printer_driver_v4.zip + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/printer_driver_adapter.zip -OutFile printer_driver_adapter.zip + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/sha256sums -OutFile sha256sums + + # Check and move the files + $checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4\.zip$').Matches.Groups[1].Value + $downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4.zip -Algorithm SHA256 + $checksum_dll = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value + $downloadsum_dll = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256 + if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_dll -eq $downloadsum_dll.Hash) { + Write-Output "rustdesk_printer_driver_v4, checksums match, extract the file." + Expand-Archive rustdesk_printer_driver_v4.zip -DestinationPath . + mkdir ./rustdesk/drivers + mv -Force .\rustdesk_printer_driver_v4 ./rustdesk/drivers/RustDeskPrinterDriver + Expand-Archive printer_driver_adapter.zip -DestinationPath . + mv -Force .\printer_driver_adapter.dll ./rustdesk + } elseif ($checksum_driver -ne $downloadsum_driver.Hash) { + Write-Output "rustdesk_printer_driver_v4, checksums do not match, ignore the file." + } else { + Write-Output "printer_driver_adapter.dll, checksums do not match, ignore the file." + } + } catch { + Write-Host "Ingore the printer driver error." + } + - name: find Runner.res # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res # Runner.rc does not contain actual version, but Runner.res does diff --git a/Cargo.lock b/Cargo.lock index f4f61833dea..eded3d37fd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5559,6 +5559,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "remote_printer" +version = "0.1.0" +dependencies = [ + "hbb_common", + "winapi 0.3.9", + "windows-strings", +] + [[package]] name = "repng" version = "0.2.2" @@ -5805,6 +5814,7 @@ dependencies = [ "percent-encoding", "qrcode-generator", "rdev", + "remote_printer", "repng", "reqwest", "ringbuf", @@ -7833,6 +7843,12 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + [[package]] name = "windows-result" version = "0.1.2" @@ -7853,6 +7869,15 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 317ab123c55..54550d6546c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,10 +116,12 @@ winapi = { version = "0.3", features = [ "cguid", "cfgmgr32", "ioapiset", + "winspool", ] } winreg = "0.11" windows-service = "0.6" virtual_display = { path = "libs/virtual_display" } +remote_printer = { path = "libs/remote_printer" } impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" } shared_memory = "0.12" tauri-winrt-notification = "0.1.2" @@ -177,7 +179,7 @@ jni = "0.21" 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"] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"] exclude = ["vdi/host", "examples/custom_plugin"] [package.metadata.winres] diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 5d43d7ae1cc..f972766623c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3789,3 +3789,29 @@ void updateTextAndPreserveSelection( baseOffset: 0, extentOffset: controller.value.text.length); } } + +List getPrinterNames() { + final printerNamesJson = bind.mainGetPrinterNames(); + if (printerNamesJson.isEmpty) { + return []; + } + try { + final List printerNamesList = jsonDecode(printerNamesJson); + final appPrinterName = '$appName Printer'; + return printerNamesList + .map((e) => e.toString()) + .where((name) => name != appPrinterName) + .toList(); + } catch (e) { + debugPrint('failed to parse printer names, err: $e'); + return []; + } +} + +String _appName = ''; +String get appName { + if (_appName.isEmpty) { + _appName = bind.mainGetAppNameSync(); + } + return _appName; +} diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 345a140ad16..302b6b440dd 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/consts.dart'; diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 011709cc598..63023443e5b 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -98,6 +98,7 @@ const String kOptionVideoSaveDirectory = "video-save-directory"; const String kOptionAccessMode = "access-mode"; const String kOptionEnableKeyboard = "enable-keyboard"; // "Settings -> Security -> Permissions" +const String kOptionEnableRemotePrinter = "enable-remote-printer"; const String kOptionEnableClipboard = "enable-clipboard"; const String kOptionEnableFileTransfer = "enable-file-transfer"; const String kOptionEnableAudio = "enable-audio"; @@ -219,6 +220,14 @@ const double kDefaultQuality = 50; const double kMaxQuality = 100; const double kMaxMoreQuality = 2000; +const String kKeyPrinterIncommingJobAction = 'printer-incomming-job-action'; +const String kValuePrinterIncomingJobDismiss = 'dismiss'; +const String kValuePrinterIncomingJobDefault = ''; +const String kValuePrinterIncomingJobSelected = 'selected'; +const String kKeyPrinterSelected = 'printer-selected-name'; +const String kKeyPrinterSave = 'allow-printer-dialog-save'; +const String kKeyPrinterAllowAutoPrint = 'allow-printer-auto-print'; + double kNewWindowOffset = isWindows ? 56.0 : isLinux diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 5d95c8d0cf6..d21e7d347c5 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -13,6 +13,7 @@ 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/printer_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'; @@ -55,6 +56,7 @@ enum SettingsTabKey { display, plugin, account, + printer, about, } @@ -74,6 +76,7 @@ class DesktopSettingPage extends StatefulWidget { if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled()) SettingsTabKey.plugin, if (!bind.isDisableAccount()) SettingsTabKey.account, + if (isWindows) SettingsTabKey.printer, SettingsTabKey.about, ]; @@ -198,6 +201,10 @@ class _DesktopSettingPageState extends State settingTabs.add( _TabInfo(tab, 'Account', Icons.person_outline, Icons.person)); break; + case SettingsTabKey.printer: + settingTabs + .add(_TabInfo(tab, 'Printer', Icons.print_outlined, Icons.print)); + break; case SettingsTabKey.about: settingTabs .add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info)); @@ -229,6 +236,9 @@ class _DesktopSettingPageState extends State case SettingsTabKey.account: children.add(const _Account()); break; + case SettingsTabKey.printer: + children.add(const _Printer()); + break; case SettingsTabKey.about: children.add(const _About()); break; @@ -963,6 +973,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { _OptionCheckBox( context, 'Enable keyboard/mouse', kOptionEnableKeyboard, enabled: enabled, fakeValue: fakeValue), + if (isWindows) + _OptionCheckBox( + context, 'Enable remote printer', kOptionEnableRemotePrinter, + enabled: enabled, fakeValue: fakeValue), _OptionCheckBox(context, 'Enable clipboard', kOptionEnableClipboard, enabled: enabled, fakeValue: fakeValue), _OptionCheckBox( @@ -1881,6 +1895,153 @@ class _PluginState extends State<_Plugin> { } } +class _Printer extends StatefulWidget { + const _Printer({super.key}); + + @override + State<_Printer> createState() => __PrinterState(); +} + +class __PrinterState extends State<_Printer> { + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return ListView(controller: scrollController, children: [ + outgoing(context), + incomming(context), + ]).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget outgoing(BuildContext context) { + final isSupportPrinterDriver = + bind.mainGetCommonSync(key: 'is-support-printer-driver') == 'true'; + + Widget tipOsNotSupported() { + return Align( + alignment: Alignment.topLeft, + child: Text(translate('printer-os-requirement-tip')), + ).marginOnly(left: _kCardLeftMargin); + } + + Widget tipClientNotInstalled() { + return Align( + alignment: Alignment.topLeft, + child: + Text(translate('printer-requires-installed-{$appName}-client-tip')), + ).marginOnly(left: _kCardLeftMargin); + } + + Widget tipPrinterNotInstalled() { + final failedMsg = ''.obs; + platformFFI.registerEventHandler( + 'install-printer-res', 'install-printer-res', (evt) async { + if (evt['success'] as bool) { + setState(() {}); + } else { + failedMsg.value = evt['msg'] as String; + } + }, replace: true); + return Column(children: [ + Obx( + () => failedMsg.value.isNotEmpty + ? Offstage() + : Align( + alignment: Alignment.topLeft, + child: Text(translate('printer-{$appName}-not-installed-tip')) + .marginOnly(bottom: 10.0), + ), + ), + Obx( + () => failedMsg.value.isEmpty + ? Offstage() + : Align( + alignment: Alignment.topLeft, + child: Text(failedMsg.value, + style: DefaultTextStyle.of(context) + .style + .copyWith(color: Colors.red)) + .marginOnly(bottom: 10.0)), + ), + _Button('Install {$appName} Printer', () { + failedMsg.value = ''; + bind.mainSetCommon(key: 'install-printer', value: ''); + }) + ]).marginOnly(left: _kCardLeftMargin, bottom: 2.0); + } + + Widget tipReady() { + return Align( + alignment: Alignment.topLeft, + child: Text(translate('printer-{$appName}-ready-tip')), + ).marginOnly(left: _kCardLeftMargin); + } + + final installed = bind.mainIsInstalled(); + // `is-printer-installed` may fail, but it's rare case. + // Add additional error message here if it's really needed. + final driver_installed = + bind.mainGetCommonSync(key: 'is-printer-installed') == 'true'; + + final List children = []; + if (!isSupportPrinterDriver) { + children.add(tipOsNotSupported()); + } else { + children.addAll([ + if (!installed) tipClientNotInstalled(), + if (installed && !driver_installed) tipPrinterNotInstalled(), + if (installed && driver_installed) tipReady() + ]); + } + return _Card(title: 'Outgoing Print Jobs', children: children); + } + + Widget incomming(BuildContext context) { + onRadioChanged(String value) async { + await bind.mainSetLocalOption( + key: kKeyPrinterIncommingJobAction, value: value); + setState(() {}); + } + + PrinterOptions printerOptions = PrinterOptions.load(); + return _Card(title: 'Incomming Print Jobs', children: [ + _Radio(context, + value: kValuePrinterIncomingJobDismiss, + groupValue: printerOptions.action, + label: 'Dismiss', + onChanged: onRadioChanged), + _Radio(context, + value: kValuePrinterIncomingJobDefault, + groupValue: printerOptions.action, + label: 'use-the-default-printer-tip', + onChanged: onRadioChanged), + _Radio(context, + value: kValuePrinterIncomingJobSelected, + groupValue: printerOptions.action, + label: 'use-the-selected-printer-tip', + onChanged: onRadioChanged), + if (printerOptions.printerNames.isNotEmpty) + ComboBox( + initialKey: printerOptions.printerName, + keys: printerOptions.printerNames, + values: printerOptions.printerNames, + enabled: printerOptions.action == kValuePrinterIncomingJobSelected, + onChanged: (value) async { + await bind.mainSetLocalOption( + key: kKeyPrinterSelected, value: value); + setState(() {}); + }, + ).marginOnly(left: 10), + _OptionCheckBox( + context, + 'auto-print-tip', + kKeyPrinterAllowAutoPrint, + isServer: false, + enabled: printerOptions.action != kValuePrinterIncomingJobDismiss, + ) + ]); + } +} + class _About extends StatefulWidget { const _About({Key? key}) : super(key: key); diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index 756367c21f1..5bf6bafeea0 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -65,6 +65,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> late final TextEditingController controller; final RxBool startmenu = true.obs; final RxBool desktopicon = true.obs; + final RxBool printer = true.obs; final RxBool showProgress = false.obs; final RxBool btnEnabled = true.obs; @@ -79,6 +80,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> final installOptions = jsonDecode(bind.installInstallOptions()); startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0'; desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0'; + printer.value = installOptions['PRINTER'] != '0'; } @override @@ -161,7 +163,9 @@ class _InstallPageBodyState extends State<_InstallPageBody> ).marginSymmetric(vertical: 2 * em), Option(startmenu, label: 'Create start menu shortcuts') .marginOnly(bottom: 7), - Option(desktopicon, label: 'Create desktop icon'), + Option(desktopicon, label: 'Create desktop icon') + .marginOnly(bottom: 7), + Option(printer, label: 'Install {$appName} Printer'), Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( @@ -253,6 +257,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> String args = ''; if (startmenu.value) args += ' startmenu'; if (desktopicon.value) args += ' desktopicon'; + if (printer.value) args += ' printer'; bind.installInstallMe(options: args, path: controller.text); } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 4a00b803e34..fabbcc00c8d 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -30,8 +30,15 @@ enum SortBy { class JobID { int _count = 0; int next() { - _count++; - return _count; + String v = bind.mainGetCommonSync(key: 'transfer-job-id'); + try { + return int.parse(v); + } catch (e) { + // unreachable. But we still handle it to make it safe. + // If we return -1, we have to check it in the caller. + _count++; + return _count; + } } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index dd7927abcf4..8091846c64d 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -9,7 +9,6 @@ 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'; @@ -19,6 +18,7 @@ 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/printer_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -412,12 +412,186 @@ class FfiModel with ChangeNotifier { isMobile) { parent.target?.recordingModel.updateStatus(evt['start'] == 'true'); } + } else if (name == "printer_request") { + _handlePrinterRequest(evt, sessionId, peerId); } else { debugPrint('Event is not handled in the fixed branch: $name'); } }; } + _handlePrinterRequest( + Map evt, SessionID sessionId, String peerId) { + final id = evt['id']; + final path = evt['path']; + final dialogManager = parent.target!.dialogManager; + dialogManager.show((setState, close, context) { + PrinterOptions printerOptions = PrinterOptions.load(); + final saveSettings = mainGetLocalBoolOptionSync(kKeyPrinterSave).obs; + final dontShowAgain = false.obs; + final Rx selectedPrinterName = printerOptions.printerName.obs; + final printerNames = printerOptions.printerNames; + final defaultOrSelectedGroupValue = + (printerOptions.action == kValuePrinterIncomingJobDismiss + ? kValuePrinterIncomingJobDefault + : printerOptions.action) + .obs; + + onRatioChanged(String? value) { + defaultOrSelectedGroupValue.value = + value ?? kValuePrinterIncomingJobDefault; + } + + onSubmit() { + final printerName = defaultOrSelectedGroupValue.isEmpty + ? '' + : selectedPrinterName.value; + bind.sessionPrinterResponse( + sessionId: sessionId, id: id, path: path, printerName: printerName); + if (saveSettings.value || dontShowAgain.value) { + bind.mainSetLocalOption(key: kKeyPrinterSelected, value: printerName); + bind.mainSetLocalOption( + key: kKeyPrinterIncommingJobAction, + value: defaultOrSelectedGroupValue.value); + } + if (dontShowAgain.value) { + mainSetLocalBoolOption(kKeyPrinterAllowAutoPrint, true); + } + close(); + } + + onCancel() { + if (dontShowAgain.value) { + bind.mainSetLocalOption( + key: kKeyPrinterIncommingJobAction, + value: kValuePrinterIncomingJobDismiss); + } + close(); + } + + final printerItemHeight = 30.0; + final selectionAreaHeight = + printerItemHeight * min(8.0, max(printerNames.length, 3.0)); + final content = Column( + children: [ + Text(translate('print-incoming-job-confirm-tip')), + Row( + children: [ + Obx(() => Radio( + value: kValuePrinterIncomingJobDefault, + groupValue: defaultOrSelectedGroupValue.value, + onChanged: onRatioChanged)), + GestureDetector( + child: Text(translate('use-the-default-printer-tip')), + onTap: () => onRatioChanged(kValuePrinterIncomingJobDefault)), + ], + ), + Column( + children: [ + Row(children: [ + Obx(() => Radio( + value: kValuePrinterIncomingJobSelected, + groupValue: defaultOrSelectedGroupValue.value, + onChanged: onRatioChanged)), + GestureDetector( + child: Text(translate('use-the-selected-printer-tip')), + onTap: () => + onRatioChanged(kValuePrinterIncomingJobSelected)), + ]), + SizedBox( + height: selectionAreaHeight, + width: 500, + child: ListView.builder( + itemBuilder: (context, index) { + return Obx(() => GestureDetector( + child: Container( + decoration: BoxDecoration( + color: selectedPrinterName.value == + printerNames[index] + ? (defaultOrSelectedGroupValue.value == + kValuePrinterIncomingJobSelected + ? MyTheme.button + : MyTheme.button.withOpacity(0.5)) + : Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(5.0), + ), + ), + key: ValueKey(printerNames[index]), + height: printerItemHeight, + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text( + printerNames[index], + style: TextStyle(fontSize: 14), + ), + ), + ), + ), + onTap: defaultOrSelectedGroupValue.value == + kValuePrinterIncomingJobSelected + ? () { + selectedPrinterName.value = + printerNames[index]; + } + : null, + )); + }, + itemCount: printerNames.length), + ), + ], + ), + Row( + children: [ + Obx(() => Checkbox( + value: saveSettings.value, + onChanged: (value) { + if (value != null) { + saveSettings.value = value; + mainSetLocalBoolOption(kKeyPrinterSave, value); + } + })), + GestureDetector( + child: Text(translate('save-settings-tip')), + onTap: () { + saveSettings.value = !saveSettings.value; + mainSetLocalBoolOption(kKeyPrinterSave, saveSettings.value); + }), + ], + ), + Row( + children: [ + Obx(() => Checkbox( + value: dontShowAgain.value, + onChanged: (value) { + if (value != null) { + dontShowAgain.value = value; + } + })), + GestureDetector( + child: Text(translate('dont-show-again-tip')), + onTap: () { + dontShowAgain.value = !dontShowAgain.value; + }), + ], + ), + ], + ); + return CustomAlertDialog( + title: Text(translate('Incoming Print Job')), + content: content, + actions: [ + dialogButton('OK', onPressed: onSubmit), + dialogButton('Cancel', onPressed: onCancel), + ], + onSubmit: onSubmit, + onCancel: onCancel, + ); + }); + } + _handleUseTextureRender( Map evt, SessionID sessionId, String peerId) { parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y'); diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index c8d5085e897..337f532786e 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -60,14 +60,14 @@ class PlatformFFI { } bool registerEventHandler( - String eventName, String handlerName, HandleEvent handler) { + String eventName, String handlerName, HandleEvent handler, {bool replace = false}) { debugPrint('registerEventHandler $eventName $handlerName'); var handlers = _eventHandlers[eventName]; if (handlers == null) { _eventHandlers[eventName] = {handlerName: handler}; return true; } else { - if (handlers.containsKey(handlerName)) { + if (!replace && handlers.containsKey(handlerName)) { return false; } else { handlers[handlerName] = handler; diff --git a/flutter/lib/models/printer_model.dart b/flutter/lib/models/printer_model.dart new file mode 100644 index 00000000000..df8f67cff37 --- /dev/null +++ b/flutter/lib/models/printer_model.dart @@ -0,0 +1,48 @@ +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; + +class PrinterOptions { + String action; + List printerNames; + String printerName; + + PrinterOptions( + {required this.action, + required this.printerNames, + required this.printerName}); + + static PrinterOptions load() { + var action = bind.mainGetLocalOption(key: kKeyPrinterIncommingJobAction); + if (![ + kValuePrinterIncomingJobDismiss, + kValuePrinterIncomingJobDefault, + kValuePrinterIncomingJobSelected + ].contains(action)) { + action = kValuePrinterIncomingJobDefault; + } + + final printerNames = getPrinterNames(); + var selectedPrinterName = bind.mainGetLocalOption(key: kKeyPrinterSelected); + if (!printerNames.contains(selectedPrinterName)) { + if (action == kValuePrinterIncomingJobSelected) { + action = kValuePrinterIncomingJobDefault; + bind.mainSetLocalOption( + key: kKeyPrinterIncommingJobAction, + value: kValuePrinterIncomingJobDefault); + if (printerNames.isEmpty) { + selectedPrinterName = ''; + } else { + selectedPrinterName = printerNames.first; + } + bind.mainSetLocalOption( + key: kKeyPrinterSelected, value: selectedPrinterName); + } + } + + return PrinterOptions( + action: action, + printerNames: printerNames, + printerName: selectedPrinterName); + } +} diff --git a/libs/hbb_common b/libs/hbb_common index 1819875476d..9ede5d49f65 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 1819875476d487612a654099881b8a16f4337599 +Subproject commit 9ede5d49f65b8c513fe613dbfea2daf7694cba3e diff --git a/libs/remote_printer/Cargo.toml b/libs/remote_printer/Cargo.toml new file mode 100644 index 00000000000..30f8aff33cd --- /dev/null +++ b/libs/remote_printer/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "remote_printer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[target.'cfg(target_os = "windows")'.dependencies] +hbb_common = { version = "0.1.0", path = "../hbb_common" } +winapi = { version = "0.3" } +windows-strings = "0.3.1" diff --git a/libs/remote_printer/src/lib.rs b/libs/remote_printer/src/lib.rs new file mode 100644 index 00000000000..51ee3721a04 --- /dev/null +++ b/libs/remote_printer/src/lib.rs @@ -0,0 +1,34 @@ +#[cfg(target_os = "windows")] +mod setup; +#[cfg(target_os = "windows")] +pub use setup::{ + is_rd_printer_installed, + setup::{install_update_printer, uninstall_printer}, +}; + +#[cfg(target_os = "windows")] +const RD_DRIVER_INF_PATH: &str = "drivers/RustDeskPrinterDriver/RustDeskPrinterDriver.inf"; + +#[cfg(target_os = "windows")] +fn get_printer_name(app_name: &str) -> Vec { + format!("{} Printer", app_name) + .encode_utf16() + .chain(Some(0)) + .collect() +} + +#[cfg(target_os = "windows")] +fn get_driver_name() -> Vec { + "RustDesk v4 Printer Driver" + .encode_utf16() + .chain(Some(0)) + .collect() +} + +#[cfg(target_os = "windows")] +fn get_port_name(app_name: &str) -> Vec { + format!("{} Printer", app_name) + .encode_utf16() + .chain(Some(0)) + .collect() +} diff --git a/libs/remote_printer/src/setup/driver.rs b/libs/remote_printer/src/setup/driver.rs new file mode 100644 index 00000000000..81226c5cc17 --- /dev/null +++ b/libs/remote_printer/src/setup/driver.rs @@ -0,0 +1,202 @@ +use super::{common_enum, get_wstr_bytes, is_name_equal}; +use hbb_common::{bail, log, ResultType}; +use std::{io, ptr::null_mut, time::Duration}; +use winapi::{ + shared::{ + minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD, MAX_PATH}, + ntdef::{DWORDLONG, LPCWSTR}, + winerror::{ERROR_UNKNOWN_PRINTER_DRIVER, S_OK}, + }, + um::{ + winspool::{ + DeletePrinterDriverExW, DeletePrinterDriverPackageW, EnumPrinterDriversW, + InstallPrinterDriverFromPackageW, UploadPrinterDriverPackageW, DPD_DELETE_ALL_FILES, + DRIVER_INFO_6W, DRIVER_INFO_8W, IPDFP_COPY_ALL_FILES, UPDP_SILENT_UPLOAD, + UPDP_UPLOAD_ALWAYS, + }, + winuser::GetForegroundWindow, + }, +}; +use windows_strings::PCWSTR; + +const HRESULT_ERR_ELEMENT_NOT_FOUND: u32 = 0x80070490; + +fn enum_printer_driver( + level: DWORD, + p_driver_info: LPBYTE, + cb_buf: DWORD, + pcb_needed: LPDWORD, + pc_returned: LPDWORD, +) -> BOOL { + unsafe { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinterdrivers + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + EnumPrinterDriversW( + null_mut(), + null_mut(), + level, + p_driver_info, + cb_buf, + pcb_needed, + pc_returned, + ) + } +} + +pub fn get_installed_driver_version(name: &PCWSTR) -> ResultType> { + common_enum( + "EnumPrinterDriversW", + enum_printer_driver, + 6, + |info: &DRIVER_INFO_6W| { + if is_name_equal(name, info.pName) { + Some(info.dwlDriverVersion) + } else { + None + } + }, + || None, + ) +} + +fn find_inf(name: &PCWSTR) -> ResultType> { + let r = common_enum( + "EnumPrinterDriversW", + enum_printer_driver, + 8, + |info: &DRIVER_INFO_8W| { + if is_name_equal(name, info.pName) { + Some(get_wstr_bytes(info.pszInfPath)) + } else { + None + } + }, + || None, + )?; + Ok(r.unwrap_or(vec![])) +} + +fn delete_printer_driver(name: &PCWSTR) -> ResultType<()> { + unsafe { + // If the printer is used after the spooler service is started. E.g., printing a document through RustDesk Printer. + // `DeletePrinterDriverExW()` may fail with `ERROR_PRINTER_DRIVER_IN_USE`(3001, 0xBB9). + // We can only ignore this error for now. + // Though restarting the spooler service is a solution, it's not a good idea to restart the service. + // + // Deleting the printer driver after deleting the printer is a common practice. + // No idea why `DeletePrinterDriverExW()` fails with `ERROR_UNKNOWN_PRINTER_DRIVER` after using the printer once. + // https://github.com/ChromiumWebApps/chromium/blob/c7361d39be8abd1574e6ce8957c8dbddd4c6ccf7/cloud_print/virtual_driver/win/install/setup.cc#L422 + // AnyDesk printer driver and the simplest printer driver also have the same issue. + if FALSE + == DeletePrinterDriverExW( + null_mut(), + null_mut(), + name.as_ptr() as _, + DPD_DELETE_ALL_FILES, + 0, + ) + { + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(ERROR_UNKNOWN_PRINTER_DRIVER as _) { + return Ok(()); + } else { + bail!("Failed to delete the printer driver, {}", err) + } + } + } + Ok(()) +} + +// https://github.com/dvalter/chromium-android-ext-dev/blob/dab74f7d5bc5a8adf303090ee25c611b4d54e2db/cloud_print/virtual_driver/win/install/setup.cc#L190 +fn delete_printer_driver_package(inf: Vec) -> ResultType<()> { + if inf.is_empty() { + return Ok(()); + } + let slen = if inf[inf.len() - 1] == 0 { + inf.len() - 1 + } else { + inf.len() + }; + let inf_path = String::from_utf16_lossy(&inf[..slen]); + if !std::path::Path::new(&inf_path).exists() { + return Ok(()); + } + + let mut retries = 3; + loop { + unsafe { + let res = DeletePrinterDriverPackageW(null_mut(), inf.as_ptr(), null_mut()); + if res == S_OK || res == HRESULT_ERR_ELEMENT_NOT_FOUND as i32 { + return Ok(()); + } + log::error!("Failed to delete the printer driver, result: {}", res); + } + retries -= 1; + if retries <= 0 { + bail!("Failed to delete the printer driver"); + } + std::thread::sleep(Duration::from_secs(2)); + } +} + +pub fn uninstall_driver(name: &PCWSTR) -> ResultType<()> { + // Note: inf must be found before `delete_printer_driver()`. + let inf = find_inf(name)?; + delete_printer_driver(name)?; + delete_printer_driver_package(inf) +} + +pub fn install_driver(name: &PCWSTR, inf: LPCWSTR) -> ResultType<()> { + let mut size = (MAX_PATH * 10) as u32; + let mut package_path = [0u16; MAX_PATH * 10]; + unsafe { + let mut res = UploadPrinterDriverPackageW( + null_mut(), + inf, + null_mut(), + UPDP_SILENT_UPLOAD | UPDP_UPLOAD_ALWAYS, + null_mut(), + package_path.as_mut_ptr(), + &mut size as _, + ); + if res != S_OK { + log::error!( + "Failed to upload the printer driver package to the driver cache silently, {}. Will try with user UI.", + res + ); + + res = UploadPrinterDriverPackageW( + null_mut(), + inf, + null_mut(), + UPDP_UPLOAD_ALWAYS, + GetForegroundWindow(), + package_path.as_mut_ptr(), + &mut size as _, + ); + if res != S_OK { + bail!( + "Failed to upload the printer driver package to the driver cache with UI, {}", + res + ); + } + } + + // https://learn.microsoft.com/en-us/windows/win32/printdocs/installprinterdriverfrompackage + res = InstallPrinterDriverFromPackageW( + null_mut(), + package_path.as_ptr(), + name.as_ptr(), + null_mut(), + IPDFP_COPY_ALL_FILES, + ); + if res != S_OK { + bail!("Failed to install the printer driver from package, {}", res); + } + } + + Ok(()) +} diff --git a/libs/remote_printer/src/setup/mod.rs b/libs/remote_printer/src/setup/mod.rs new file mode 100644 index 00000000000..ddf386de348 --- /dev/null +++ b/libs/remote_printer/src/setup/mod.rs @@ -0,0 +1,99 @@ +use hbb_common::{bail, ResultType}; +use std::{io, ptr::null_mut}; +use winapi::{ + shared::{ + minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD}, + ntdef::{LPCWSTR, LPWSTR}, + }, + um::winbase::{lstrcmpiW, lstrlenW}, +}; +use windows_strings::PCWSTR; + +mod driver; +mod port; +pub(crate) mod printer; +pub(crate) mod setup; + +#[inline] +pub fn is_rd_printer_installed(app_name: &str) -> ResultType { + let printer_name = crate::get_printer_name(app_name); + let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr()); + printer::is_printer_added(&rd_printer_name) +} + +fn get_wstr_bytes(p: LPWSTR) -> Vec { + let mut vec_bytes = vec![]; + unsafe { + let len: isize = lstrlenW(p) as _; + if len > 0 { + for i in 0..len + 1 { + vec_bytes.push(*p.offset(i)); + } + } + } + vec_bytes +} + +fn is_name_equal(name: &PCWSTR, name_from_api: LPCWSTR) -> bool { + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lstrcmpiw + // For some locales, the lstrcmpi function may be insufficient. + // If this occurs, use `CompareStringEx` to ensure proper comparison. + // For example, in Japan call with the NORM_IGNORECASE, NORM_IGNOREKANATYPE, and NORM_IGNOREWIDTH values to achieve the most appropriate non-exact string comparison. + // Note that specifying these values slows performance, so use them only when necessary. + // + // No need to consider `CompareStringEx` for now. + unsafe { lstrcmpiW(name.as_ptr(), name_from_api) == 0 } +} + +fn common_enum( + enum_name: &str, + enum_fn: fn( + Level: DWORD, + pDriverInfo: LPBYTE, + cbBuf: DWORD, + pcbNeeded: LPDWORD, + pcReturned: LPDWORD, + ) -> BOOL, + level: DWORD, + on_data: impl Fn(&T) -> Option, + on_no_data: impl Fn() -> Option, +) -> ResultType> { + let mut needed = 0; + let mut returned = 0; + enum_fn(level, null_mut(), 0, &mut needed, &mut returned); + if needed == 0 { + return Ok(on_no_data()); + } + + let mut buffer = vec![0u8; needed as usize]; + if FALSE + == enum_fn( + level, + buffer.as_mut_ptr(), + needed, + &mut needed, + &mut returned, + ) + { + bail!( + "Failed to call {}, error: {}", + enum_name, + io::Error::last_os_error() + ) + } + + // to-do: how to free the buffers in *const T? + + let p_enum_info = buffer.as_ptr() as *const T; + unsafe { + for i in 0..returned { + let enum_info = p_enum_info.offset(i as isize); + let r = on_data(&*enum_info); + if r.is_some() { + return Ok(r); + } + } + } + + Ok(on_no_data()) +} diff --git a/libs/remote_printer/src/setup/port.rs b/libs/remote_printer/src/setup/port.rs new file mode 100644 index 00000000000..d5ab0bace08 --- /dev/null +++ b/libs/remote_printer/src/setup/port.rs @@ -0,0 +1,128 @@ +use super::{common_enum, is_name_equal, printer::get_printer_installed_on_port}; +use hbb_common::{bail, ResultType}; +use std::{io, ptr::null_mut}; +use winapi::{ + shared::minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD}, + um::{ + winnt::HANDLE, + winspool::{ + ClosePrinter, EnumPortsW, OpenPrinterW, XcvDataW, PORT_INFO_2W, PRINTER_DEFAULTSW, + SERVER_WRITE, + }, + }, +}; +use windows_strings::{w, PCWSTR}; + +const XCV_MONITOR_LOCAL_PORT: PCWSTR = w!(",XcvMonitor Local Port"); + +fn enum_printer_port( + level: DWORD, + p_port_info: LPBYTE, + cb_buf: DWORD, + pcb_needed: LPDWORD, + pc_returned: LPDWORD, +) -> BOOL { + unsafe { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumports + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + EnumPortsW( + null_mut(), + level, + p_port_info, + cb_buf, + pcb_needed, + pc_returned, + ) + } +} + +fn is_port_exists(name: &PCWSTR) -> ResultType { + let r = common_enum( + "EnumPortsW", + enum_printer_port, + 2, + |info: &PORT_INFO_2W| { + if is_name_equal(name, info.pPortName) { + Some(true) + } else { + None + } + }, + || None, + )?; + Ok(r.unwrap_or(false)) +} + +unsafe fn execute_on_local_port(port: &PCWSTR, command: &PCWSTR) -> ResultType<()> { + let mut dft = PRINTER_DEFAULTSW { + pDataType: null_mut(), + pDevMode: null_mut(), + DesiredAccess: SERVER_WRITE, + }; + let mut h_monitor: HANDLE = null_mut(); + if FALSE + == OpenPrinterW( + XCV_MONITOR_LOCAL_PORT.as_ptr() as _, + &mut h_monitor, + &mut dft as *mut PRINTER_DEFAULTSW as _, + ) + { + bail!(format!( + "Failed to open Local Port monitor. Error: {}", + io::Error::last_os_error() + )) + } + + let mut output_needed: u32 = 0; + let mut status: u32 = 0; + if FALSE + == XcvDataW( + h_monitor, + command.as_ptr(), + port.as_ptr() as *mut u8, + (port.len() + 1) as u32 * 2, + null_mut(), + 0, + &mut output_needed, + &mut status, + ) + { + ClosePrinter(h_monitor); + bail!(format!( + "Failed to execute the command on the printer port, Error: {}", + io::Error::last_os_error() + )) + } + + ClosePrinter(h_monitor); + + Ok(()) +} + +fn add_local_port(port: &PCWSTR) -> ResultType<()> { + unsafe { execute_on_local_port(port, &w!("AddPort")) } +} + +fn delete_local_port(port: &PCWSTR) -> ResultType<()> { + unsafe { execute_on_local_port(port, &w!("DeletePort")) } +} + +pub fn check_add_local_port(port: &PCWSTR) -> ResultType<()> { + if !is_port_exists(port)? { + return add_local_port(port); + } + Ok(()) +} + +pub fn check_delete_local_port(port: &PCWSTR) -> ResultType<()> { + if is_port_exists(port)? { + if get_printer_installed_on_port(port)?.is_some() { + bail!("The printer is installed on the port. Please remove the printer first."); + } + return delete_local_port(port); + } + Ok(()) +} diff --git a/libs/remote_printer/src/setup/printer.rs b/libs/remote_printer/src/setup/printer.rs new file mode 100644 index 00000000000..9882b8f38b9 --- /dev/null +++ b/libs/remote_printer/src/setup/printer.rs @@ -0,0 +1,161 @@ +use super::{common_enum, get_wstr_bytes, is_name_equal}; +use hbb_common::{bail, ResultType}; +use std::{io, ptr::null_mut}; +use winapi::{ + shared::{ + minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD}, + ntdef::HANDLE, + winerror::ERROR_INVALID_PRINTER_NAME, + }, + um::winspool::{ + AddPrinterW, ClosePrinter, DeletePrinter, EnumPrintersW, OpenPrinterW, SetPrinterW, + PRINTER_ALL_ACCESS, PRINTER_ATTRIBUTE_LOCAL, PRINTER_CONTROL_PURGE, PRINTER_DEFAULTSW, + PRINTER_ENUM_LOCAL, PRINTER_INFO_1W, PRINTER_INFO_2W, + }, +}; +use windows_strings::{w, PCWSTR}; + +fn enum_local_printer( + level: DWORD, + p_printer_info: LPBYTE, + cb_buf: DWORD, + pcb_needed: LPDWORD, + pc_returned: LPDWORD, +) -> BOOL { + unsafe { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinters + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + EnumPrintersW( + PRINTER_ENUM_LOCAL, + null_mut(), + level, + p_printer_info, + cb_buf, + pcb_needed, + pc_returned, + ) + } +} + +#[inline] +pub fn is_printer_added(name: &PCWSTR) -> ResultType { + let r = common_enum( + "EnumPrintersW", + enum_local_printer, + 1, + |info: &PRINTER_INFO_1W| { + if is_name_equal(name, info.pName) { + Some(true) + } else { + None + } + }, + || None, + )?; + Ok(r.unwrap_or(false)) +} + +// Only return the first matched printer +pub fn get_printer_installed_on_port(port: &PCWSTR) -> ResultType>> { + common_enum( + "EnumPrintersW", + enum_local_printer, + 2, + |info: &PRINTER_INFO_2W| { + if is_name_equal(port, info.pPortName) { + Some(get_wstr_bytes(info.pPrinterName)) + } else { + None + } + }, + || None, + ) +} + +pub fn add_printer(name: &PCWSTR, driver: &PCWSTR, port: &PCWSTR) -> ResultType<()> { + let mut printer_info = PRINTER_INFO_2W { + pServerName: null_mut(), + pPrinterName: name.as_ptr() as _, + pShareName: null_mut(), + pPortName: port.as_ptr() as _, + pDriverName: driver.as_ptr() as _, + pComment: null_mut(), + pLocation: null_mut(), + pDevMode: null_mut(), + pSepFile: null_mut(), + pPrintProcessor: w!("WinPrint").as_ptr() as _, + pDatatype: w!("RAW").as_ptr() as _, + pParameters: null_mut(), + pSecurityDescriptor: null_mut(), + Attributes: PRINTER_ATTRIBUTE_LOCAL, + Priority: 0, + DefaultPriority: 0, + StartTime: 0, + UntilTime: 0, + Status: 0, + cJobs: 0, + AveragePPM: 0, + }; + unsafe { + let h_printer = AddPrinterW( + null_mut(), + 2, + &mut printer_info as *mut PRINTER_INFO_2W as _, + ); + if h_printer.is_null() { + bail!(format!( + "Failed to add printer. Error: {}", + io::Error::last_os_error() + )) + } + } + Ok(()) +} + +pub fn delete_printer(name: &PCWSTR) -> ResultType<()> { + let mut dft = PRINTER_DEFAULTSW { + pDataType: null_mut(), + pDevMode: null_mut(), + DesiredAccess: PRINTER_ALL_ACCESS, + }; + let mut h_printer: HANDLE = null_mut(); + unsafe { + if FALSE + == OpenPrinterW( + name.as_ptr() as _, + &mut h_printer, + &mut dft as *mut PRINTER_DEFAULTSW as _, + ) + { + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(ERROR_INVALID_PRINTER_NAME as _) { + return Ok(()); + } else { + bail!(format!("Failed to open printer. Error: {}", err)) + } + } + + if FALSE == SetPrinterW(h_printer, 0, null_mut(), PRINTER_CONTROL_PURGE) { + ClosePrinter(h_printer); + bail!(format!( + "Failed to purge printer queue. Error: {}", + io::Error::last_os_error() + )) + } + + if FALSE == DeletePrinter(h_printer) { + ClosePrinter(h_printer); + bail!(format!( + "Failed to delete printer. Error: {}", + io::Error::last_os_error() + )) + } + + ClosePrinter(h_printer); + } + + Ok(()) +} diff --git a/libs/remote_printer/src/setup/setup.rs b/libs/remote_printer/src/setup/setup.rs new file mode 100644 index 00000000000..f461ab75c55 --- /dev/null +++ b/libs/remote_printer/src/setup/setup.rs @@ -0,0 +1,94 @@ +use super::{ + driver::{get_installed_driver_version, install_driver, uninstall_driver}, + port::{check_add_local_port, check_delete_local_port}, + printer::{add_printer, delete_printer}, +}; +use hbb_common::{allow_err, bail, lazy_static, log, ResultType}; +use std::{path::PathBuf, sync::Mutex}; +use windows_strings::PCWSTR; + +lazy_static::lazy_static!( + static ref SETUP_MTX: Mutex<()> = Mutex::new(()); +); + +fn get_driver_inf_abs_path() -> ResultType { + use crate::RD_DRIVER_INF_PATH; + + let exe_file = std::env::current_exe()?; + let abs_path = match exe_file.parent() { + Some(parent) => parent.join(RD_DRIVER_INF_PATH), + None => bail!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ), + }; + if !abs_path.exists() { + bail!( + "The driver inf file \"{}\" does not exists", + RD_DRIVER_INF_PATH + ) + } + Ok(abs_path) +} + +// Note: This function must be called in a separate thread. +// Because many functions in this module are blocking or synchronous. +// Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. +// Steps: +// 1. Add the local port. +// 2. Check if the driver is installed. +// Uninstall the existing driver if it is installed. +// We should not check the driver version because the driver is deployed with the application. +// It's better to uninstall the existing driver and install the driver from the application. +// 3. Add the printer. +pub fn install_update_printer(app_name: &str) -> ResultType<()> { + let printer_name = crate::get_printer_name(app_name); + let driver_name = crate::get_driver_name(); + let port = crate::get_port_name(app_name); + let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr()); + let rd_printer_driver_name = PCWSTR::from_raw(driver_name.as_ptr()); + let rd_printer_port = PCWSTR::from_raw(port.as_ptr()); + + let inf_file = get_driver_inf_abs_path()?; + let inf_file: Vec = inf_file + .to_string_lossy() + .as_ref() + .encode_utf16() + .chain(Some(0).into_iter()) + .collect(); + let _lock = SETUP_MTX.lock().unwrap(); + + check_add_local_port(&rd_printer_port)?; + + let should_install_driver = match get_installed_driver_version(&rd_printer_driver_name)? { + Some(_version) => { + delete_printer(&rd_printer_name)?; + allow_err!(uninstall_driver(&rd_printer_driver_name)); + true + } + None => true, + }; + + if should_install_driver { + allow_err!(install_driver(&rd_printer_driver_name, inf_file.as_ptr())); + } + + add_printer(&rd_printer_name, &rd_printer_driver_name, &rd_printer_port)?; + + Ok(()) +} + +pub fn uninstall_printer(app_name: &str) { + let printer_name = crate::get_printer_name(app_name); + let driver_name = crate::get_driver_name(); + let port = crate::get_port_name(app_name); + let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr()); + let rd_printer_driver_name = PCWSTR::from_raw(driver_name.as_ptr()); + let rd_printer_port = PCWSTR::from_raw(port.as_ptr()); + + let _lock = SETUP_MTX.lock().unwrap(); + + allow_err!(delete_printer(&rd_printer_name)); + allow_err!(uninstall_driver(&rd_printer_driver_name)); + allow_err!(check_delete_local_port(&rd_printer_port)); +} diff --git a/res/inline-sciter.py b/res/inline-sciter.py index 26cf1a75471..4c81bd621ae 100644 --- a/res/inline-sciter.py +++ b/res/inline-sciter.py @@ -23,7 +23,8 @@ def strip(s): return re.sub(r'\s+\n', '\n', re.sub(r'\n\s+', '\n', s)) .replace('include "grid.tis";', open('src/ui/grid.tis').read()) \ .replace('include "header.tis";', open('src/ui/header.tis').read()) \ .replace('include "file_transfer.tis";', open('src/ui/file_transfer.tis').read()) \ - .replace('include "port_forward.tis";', open('src/ui/port_forward.tis').read()) + .replace('include "port_forward.tis";', open('src/ui/port_forward.tis').read()) \ + .replace('include "printer.tis";', open('src/ui/printer.tis').read()) chatbox = open('src/ui/chatbox.html').read() install = open('src/ui/install.html').read().replace('include "install.tis";', open('src/ui/install.tis').read()) diff --git a/res/msi/CustomActions/Common.h b/res/msi/CustomActions/Common.h index 5dcd529c065..08302d98c36 100644 --- a/res/msi/CustomActions/Common.h +++ b/res/msi/CustomActions/Common.h @@ -15,3 +15,9 @@ bool MyStopServiceW(LPCWSTR serviceName); std::wstring ReadConfig(const std::wstring& filename, const std::wstring& key); void UninstallDriver(LPCWSTR hardwareId, BOOL &rebootRequired); + +namespace RemotePrinter +{ + VOID installUpdatePrinter(const std::wstring& installFolder); + VOID uninstallPrinter(); +} diff --git a/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp index afe06fdbb0b..fafbab6b5f6 100644 --- a/res/msi/CustomActions/CustomActions.cpp +++ b/res/msi/CustomActions/CustomActions.cpp @@ -878,3 +878,55 @@ void TryStopDeleteServiceByShell(LPWSTR svcName) WcaLog(LOGMSG_STANDARD, "Failed to delete service: \"%ls\" with shell, current status: %d.", svcName, svcStatus.dwCurrentState); } } + +UINT __stdcall InstallPrinter( + __in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + int nResult = 0; + LPWSTR installFolder = NULL; + LPWSTR pwz = NULL; + LPWSTR pwzData = NULL; + + hr = WcaInitialize(hInstall, "InstallPrinter"); + ExitOnFailure(hr, "Failed to initialize"); + + hr = WcaGetProperty(L"CustomActionData", &pwzData); + ExitOnFailure(hr, "failed to get CustomActionData"); + + pwz = pwzData; + hr = WcaReadStringFromCaData(&pwz, &installFolder); + ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); + + WcaLog(LOGMSG_STANDARD, "Try to install RD printer in : %ls", installFolder); + RemotePrinter::installUpdatePrinter(installFolder); + WcaLog(LOGMSG_STANDARD, "Install RD printer done"); + +LExit: + if (pwzData) { + ReleaseStr(pwzData); + } + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +UINT __stdcall UninstallPrinter( + __in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + hr = WcaInitialize(hInstall, "UninstallPrinter"); + ExitOnFailure(hr, "Failed to initialize"); + + WcaLog(LOGMSG_STANDARD, "Try to uninstall RD printer"); + RemotePrinter::uninstallPrinter(); + WcaLog(LOGMSG_STANDARD, "Uninstall RD printer done"); + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} diff --git a/res/msi/CustomActions/CustomActions.def b/res/msi/CustomActions/CustomActions.def index 557bfaf1871..01b03490c57 100644 --- a/res/msi/CustomActions/CustomActions.def +++ b/res/msi/CustomActions/CustomActions.def @@ -12,3 +12,5 @@ EXPORTS SetPropertyFromConfig AddRegSoftwareSASGeneration RemoveAmyuniIdd + InstallPrinter + UninstallPrinter diff --git a/res/msi/CustomActions/CustomActions.vcxproj b/res/msi/CustomActions/CustomActions.vcxproj index 1bff7b15402..2e704fbb5e8 100644 --- a/res/msi/CustomActions/CustomActions.vcxproj +++ b/res/msi/CustomActions/CustomActions.vcxproj @@ -67,6 +67,7 @@ Create + diff --git a/res/msi/CustomActions/RemotePrinter.cpp b/res/msi/CustomActions/RemotePrinter.cpp new file mode 100644 index 00000000000..767c8c82cef --- /dev/null +++ b/res/msi/CustomActions/RemotePrinter.cpp @@ -0,0 +1,517 @@ +#include "pch.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common.h" + +#pragma comment(lib, "setupapi.lib") +#pragma comment(lib, "winspool.lib") + +namespace RemotePrinter +{ +#define HRESULT_ERR_ELEMENT_NOT_FOUND 0x80070490 + + LPCWCH RD_DRIVER_INF_PATH = L"drivers\\RustDeskPrinterDriver\\RustDeskPrinterDriver.inf"; + LPCWCH RD_PRINTER_PORT = L"RustDesk Printer"; + LPCWCH RD_PRINTER_NAME = L"RustDesk Printer"; + LPCWCH RD_PRINTER_DRIVER_NAME = L"RustDesk v4 Printer Driver"; + LPCWCH XCV_MONITOR_LOCAL_PORT = L",XcvMonitor Local Port"; + + using FuncEnum = std::function; + template + using FuncOnData = std::function(const T &)>; + template + using FuncOnNoData = std::function()>; + + template + std::shared_ptr commonEnum(std::wstring funcName, FuncEnum func, DWORD level, FuncOnData onData, FuncOnNoData onNoData) + { + DWORD needed = 0; + DWORD returned = 0; + func(level, NULL, 0, &needed, &returned); + if (needed == 0) + { + return onNoData(); + } + + std::vector buffer(needed); + if (!func(level, buffer.data(), needed, &needed, &returned)) + { + return nullptr; + } + + T *pPortInfo = reinterpret_cast(buffer.data()); + for (DWORD i = 0; i < returned; i++) + { + auto r = onData(pPortInfo[i]); + if (r) + { + return r; + } + } + return onNoData(); + } + + BOOL isNameEqual(LPCWSTR lhs, LPCWSTR rhs) + { + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lstrcmpiw + // For some locales, the lstrcmpi function may be insufficient. + // If this occurs, use `CompareStringEx` to ensure proper comparison. + // For example, in Japan call with the NORM_IGNORECASE, NORM_IGNOREKANATYPE, and NORM_IGNOREWIDTH values to achieve the most appropriate non-exact string comparison. + // Note that specifying these values slows performance, so use them only when necessary. + // + // No need to consider `CompareStringEx` for now. + return lstrcmpiW(lhs, rhs) == 0 ? TRUE : FALSE; + } + + BOOL enumPrinterPort( + DWORD level, + LPBYTE pPortInfo, + DWORD cbBuf, + LPDWORD pcbNeeded, + LPDWORD pcReturned) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumports + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + return EnumPortsW(NULL, level, pPortInfo, cbBuf, pcbNeeded, pcReturned); + } + + BOOL isPortExists(LPCWSTR port) + { + auto onData = [port](const PORT_INFO_2 &info) + { + if (isNameEqual(info.pPortName, port) == TRUE) { + return std::shared_ptr(new BOOL(TRUE)); + } + else { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPortsW", enumPrinterPort, 2, onData, onNoData); + if (res == nullptr) + { + return false; + } + else + { + return *res; + } + } + + BOOL executeOnLocalPort(LPCWSTR port, LPCWSTR command) + { + PRINTER_DEFAULTSW dft = {0}; + dft.DesiredAccess = SERVER_WRITE; + HANDLE hMonitor = NULL; + if (OpenPrinterW(const_cast(XCV_MONITOR_LOCAL_PORT), &hMonitor, &dft) == FALSE) + { + return FALSE; + } + + DWORD outputNeeded = 0; + DWORD status = 0; + if (XcvDataW(hMonitor, command, (LPBYTE)port, (lstrlenW(port) + 1) * 2, NULL, 0, &outputNeeded, &status) == FALSE) + { + ClosePrinter(hMonitor); + return FALSE; + } + + ClosePrinter(hMonitor); + return TRUE; + } + + BOOL addLocalPort(LPCWSTR port) + { + return executeOnLocalPort(port, L"AddPort"); + } + + BOOL deleteLocalPort(LPCWSTR port) + { + return executeOnLocalPort(port, L"DeletePort"); + } + + BOOL checkAddLocalPort(LPCWSTR port) + { + if (!isPortExists(port)) + { + return addLocalPort(port); + } + return TRUE; + } + + std::wstring getPrinterInstalledOnPort(LPCWSTR port); + + BOOL checkDeleteLocalPort(LPCWSTR port) + { + if (isPortExists(port)) + { + if (getPrinterInstalledOnPort(port) != L"") + { + WcaLog(LOGMSG_STANDARD, "The printer is installed on the port. Please remove the printer first.\n"); + return FALSE; + } + return deleteLocalPort(port); + } + return TRUE; + } + + BOOL enumPrinterDriver( + DWORD level, + LPBYTE pDriverInfo, + DWORD cbBuf, + LPDWORD pcbNeeded, + LPDWORD pcReturned) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinterdrivers + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + return EnumPrinterDriversW( + NULL, + NULL, + level, + pDriverInfo, + cbBuf, + pcbNeeded, + pcReturned); + } + + DWORDLONG getInstalledDriverVersion(LPCWSTR name) + { + auto onData = [name](const DRIVER_INFO_6W &info) + { + if (isNameEqual(name, info.pName) == TRUE) + { + return std::shared_ptr(new DWORDLONG(info.dwlDriverVersion)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrinterDriversW", enumPrinterDriver, 6, onData, onNoData); + if (res == nullptr) + { + return 0; + } + else + { + return *res; + } + } + + std::wstring findInf(LPCWSTR name) + { + auto onData = [name](const DRIVER_INFO_8W &info) + { + if (isNameEqual(name, info.pName) == TRUE) + { + return std::shared_ptr(new std::wstring(info.pszInfPath)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrinterDriversW", enumPrinterDriver, 8, onData, onNoData); + if (res == nullptr) + { + return L""; + } + else + { + return *res; + } + } + + BOOL deletePrinterDriver(LPCWSTR name) + { + // If the printer is used after the spooler service is started. E.g., printing a document through RustDesk Printer. + // `DeletePrinterDriverExW()` may fail with `ERROR_PRINTER_DRIVER_IN_USE`(3001, 0xBB9). + // We can only ignore this error for now. + // Though restarting the spooler service is a solution, it's not a good idea to restart the service. + // + // Deleting the printer driver after deleting the printer is a common practice. + // No idea why `DeletePrinterDriverExW()` fails with `ERROR_UNKNOWN_PRINTER_DRIVER` after using the printer once. + // https://github.com/ChromiumWebApps/chromium/blob/c7361d39be8abd1574e6ce8957c8dbddd4c6ccf7/cloud_print/virtual_driver/win/install/setup.cc#L422 + // AnyDesk printer driver and the simplest printer driver also have the same issue. + BOOL res = DeletePrinterDriverExW(NULL, NULL, const_cast(name), DPD_DELETE_ALL_FILES, 0); + if (res == FALSE) + { + DWORD error = GetLastError(); + if (error == ERROR_UNKNOWN_PRINTER_DRIVER) + { + return TRUE; + } + else + { + WcaLog(LOGMSG_STANDARD, "Failed to delete printer driver. Error (%d)\n", error); + } + } + return res; + } + + BOOL deletePrinterDriverPackage(const std::wstring &inf) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/deleteprinterdriverpackage + // This function is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + int tries = 3; + HRESULT result = S_FALSE; + while ((result = DeletePrinterDriverPackage(NULL, inf.c_str(), NULL)) != S_OK) + { + if (result == HRESULT_ERR_ELEMENT_NOT_FOUND) + { + return TRUE; + } + + WcaLog(LOGMSG_STANDARD, "Failed to delete printer driver package. HRESULT (%d)\n", result); + tries--; + if (tries <= 0) + { + return FALSE; + } + Sleep(2000); + } + return S_OK; + } + + BOOL uninstallDriver(LPCWSTR name) + { + auto infFile = findInf(name); + if (!deletePrinterDriver(name)) + { + return FALSE; + } + if (infFile != L"" && !deletePrinterDriverPackage(infFile)) + { + return FALSE; + } + return TRUE; + } + + BOOL installDriver(LPCWSTR name, LPCWSTR inf) + { + DWORD size = MAX_PATH * 10; + wchar_t package_path[MAX_PATH * 10] = {0}; + HRESULT result = UploadPrinterDriverPackage( + NULL, inf, NULL, + UPDP_SILENT_UPLOAD | UPDP_UPLOAD_ALWAYS, NULL, package_path, &size); + if (result != S_OK) + { + WcaLog(LOGMSG_STANDARD, "Uploading the printer driver package to the driver cache silently, failed. Will retry with user UI. HRESULT (%d)\n", result); + result = UploadPrinterDriverPackage( + NULL, inf, NULL, UPDP_UPLOAD_ALWAYS, + GetForegroundWindow(), package_path, &size); + if (result != S_OK) + { + WcaLog(LOGMSG_STANDARD, "Uploading the printer driver package to the driver cache failed with user UI. Aborting...\n"); + return FALSE; + } + } + + result = InstallPrinterDriverFromPackage( + NULL, package_path, name, NULL, IPDFP_COPY_ALL_FILES); + if (result != S_OK) + { + WcaLog(LOGMSG_STANDARD, "Installing the printer driver failed. HRESULT (%d)\n", result); + } + return result == S_OK; + } + + BOOL enumLocalPrinter( + DWORD level, + LPBYTE pPrinterInfo, + DWORD cbBuf, + LPDWORD pcbNeeded, + LPDWORD pcReturned) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinters + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + return EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, level, pPrinterInfo, cbBuf, pcbNeeded, pcReturned); + } + + BOOL isPrinterAdded(LPCWSTR name) + { + auto onData = [name](const PRINTER_INFO_1W &info) + { + if (isNameEqual(name, info.pName) == TRUE) + { + return std::shared_ptr(new BOOL(TRUE)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrintersW", enumLocalPrinter, 1, onData, onNoData); + if (res == nullptr) + { + return FALSE; + } + else + { + return *res; + } + } + + std::wstring getPrinterInstalledOnPort(LPCWSTR port) + { + auto onData = [port](const PRINTER_INFO_2W &info) + { + if (isNameEqual(port, info.pPortName) == TRUE) + { + return std::shared_ptr(new std::wstring(info.pPrinterName)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrintersW", enumLocalPrinter, 2, onData, onNoData); + if (res == nullptr) + { + return L""; + } + else + { + return *res; + } + } + + BOOL addPrinter(LPCWSTR name, LPCWSTR driver, LPCWSTR port) + { + PRINTER_INFO_2W printerInfo = {0}; + printerInfo.pPrinterName = const_cast(name); + printerInfo.pPortName = const_cast(port); + printerInfo.pDriverName = const_cast(driver); + printerInfo.pPrintProcessor = const_cast(L"WinPrint"); + printerInfo.pDatatype = const_cast(L"RAW"); + printerInfo.Attributes = PRINTER_ATTRIBUTE_LOCAL; + HANDLE hPrinter = AddPrinterW(NULL, 2, (LPBYTE)&printerInfo); + return hPrinter == NULL ? FALSE : TRUE; + } + + VOID deletePrinter(LPCWSTR name) + { + PRINTER_DEFAULTSW dft = {0}; + dft.DesiredAccess = PRINTER_ALL_ACCESS; + HANDLE hPrinter = NULL; + if (OpenPrinterW(const_cast(name), &hPrinter, &dft) == FALSE) + { + DWORD error = GetLastError(); + if (error == ERROR_INVALID_PRINTER_NAME) + { + return; + } + WcaLog(LOGMSG_STANDARD, "Failed to open printer. error (%d)\n", error); + return; + } + + if (SetPrinterW(hPrinter, 0, NULL, PRINTER_CONTROL_PURGE) == FALSE) + { + ClosePrinter(hPrinter); + WcaLog(LOGMSG_STANDARD, "Failed to purge printer queue. error (%d)\n", GetLastError()); + return; + } + + if (DeletePrinter(hPrinter) == FALSE) + { + ClosePrinter(hPrinter); + WcaLog(LOGMSG_STANDARD, "Failed to delete printer. error (%d)\n", GetLastError()); + return; + } + + ClosePrinter(hPrinter); + } + + bool FileExists(const std::wstring &filePath) + { + DWORD fileAttributes = GetFileAttributes(filePath.c_str()); + return (fileAttributes != INVALID_FILE_ATTRIBUTES && !(fileAttributes & FILE_ATTRIBUTE_DIRECTORY)); + } + + // Steps: + // 1. Add the local port. + // 2. Check if the driver is installed. + // Uninstall the existing driver if it is installed. + // We should not check the driver version because the driver is deployed with the application. + // It's better to uninstall the existing driver and install the driver from the application. + // 3. Add the printer. + VOID installUpdatePrinter(const std::wstring &installFolder) + { + const std::wstring infFile = installFolder + L"\\" + RemotePrinter::RD_DRIVER_INF_PATH; + if (!FileExists(infFile)) + { + WcaLog(LOGMSG_STANDARD, "Printer driver INF file not found, aborting...\n"); + return; + } + + if (!checkAddLocalPort(RD_PRINTER_PORT)) + { + WcaLog(LOGMSG_STANDARD, "Failed to check add local port, error (%d)\n", GetLastError()); + return; + } + else + { + WcaLog(LOGMSG_STANDARD, "Local port added successfully\n"); + } + + if (getInstalledDriverVersion(RD_PRINTER_DRIVER_NAME) > 0) + { + deletePrinter(RD_PRINTER_NAME); + if (FALSE == uninstallDriver(RD_PRINTER_DRIVER_NAME)) + { + WcaLog(LOGMSG_STANDARD, "Failed to uninstall previous printer driver, error (%d)\n", GetLastError()); + } + } + + if (FALSE == installDriver(RD_PRINTER_DRIVER_NAME, infFile.c_str())) + { + WcaLog(LOGMSG_STANDARD, "Driver installation failed, still try to add the printer\n"); + } + else + { + WcaLog(LOGMSG_STANDARD, "Driver installed successfully\n"); + } + + if (FALSE == addPrinter(RD_PRINTER_NAME, RD_PRINTER_DRIVER_NAME, RD_PRINTER_PORT)) + { + WcaLog(LOGMSG_STANDARD, "Failed to add printer, error (%d)\n", GetLastError()); + } + else + { + WcaLog(LOGMSG_STANDARD, "Printer installed successfully\n"); + } + } + + VOID uninstallPrinter() + { + deletePrinter(RD_PRINTER_NAME); + WcaLog(LOGMSG_STANDARD, "Deleted the printer\n"); + uninstallDriver(RD_PRINTER_DRIVER_NAME); + WcaLog(LOGMSG_STANDARD, "Uninstalled the printer driver\n"); + checkDeleteLocalPort(RD_PRINTER_PORT); + WcaLog(LOGMSG_STANDARD, "Deleted the local port\n"); + } +} diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index ff39f844344..4093a0189f2 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -30,6 +30,7 @@ + @@ -57,6 +58,18 @@ + + + + + + + @@ -72,6 +85,8 @@ + + diff --git a/res/msi/Package/Fragments/CustomActions.wxs b/res/msi/Package/Fragments/CustomActions.wxs index b443eff52c5..3727c0dd3bf 100644 --- a/res/msi/Package/Fragments/CustomActions.wxs +++ b/res/msi/Package/Fragments/CustomActions.wxs @@ -17,5 +17,7 @@ + + diff --git a/res/msi/Package/Fragments/ShortcutProperties.wxs b/res/msi/Package/Fragments/ShortcutProperties.wxs index 95bd4aaa370..dafd7dea485 100644 --- a/res/msi/Package/Fragments/ShortcutProperties.wxs +++ b/res/msi/Package/Fragments/ShortcutProperties.wxs @@ -14,6 +14,7 @@ + @@ -23,6 +24,9 @@ + + + @@ -46,6 +50,16 @@ + + + + + + + + + + + + + diff --git a/res/msi/Package/Language/Package.en-us.wxl b/res/msi/Package/Language/Package.en-us.wxl index 1bd3986ddbb..c65a5126d4a 100644 --- a/res/msi/Package/Language/Package.en-us.wxl +++ b/res/msi/Package/Language/Package.en-us.wxl @@ -51,5 +51,6 @@ 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 bdd8471cfc0..e11756a6506 100644 --- a/res/msi/Package/Package.wxs +++ b/res/msi/Package/Package.wxs @@ -51,6 +51,8 @@ + + diff --git a/res/msi/Package/UI/MyInstallDirDlg.wxs b/res/msi/Package/UI/MyInstallDirDlg.wxs index 6e27e2b2826..e4bad91972b 100644 --- a/res/msi/Package/UI/MyInstallDirDlg.wxs +++ b/res/msi/Package/UI/MyInstallDirDlg.wxs @@ -25,6 +25,7 @@ + diff --git a/res/msi/preprocess.py b/res/msi/preprocess.py index 9a43e9da6a9..670091ad8fb 100644 --- a/res/msi/preprocess.py +++ b/res/msi/preprocess.py @@ -8,7 +8,9 @@ import datetime import subprocess import re +import platform from pathlib import Path +from itertools import chain import shutil g_indent_unit = "\t" @@ -187,6 +189,17 @@ def replace_app_name_in_langs(app_name): with open(file_path, "w", encoding="utf-8") as f: f.writelines(lines) +def replace_app_name_in_custom_actions(app_name): + custion_actions_dir = Path(sys.argv[0]).parent.joinpath("CustomActions") + for file_path in chain(custion_actions_dir.glob("*.cpp"), custion_actions_dir.glob("*.h")): + with open(file_path, "r", encoding="utf-8") as f: + lines = f.readlines() + for i, line in enumerate(lines): + line = re.sub(r"\bRustDesk\b", app_name, line) + line = line.replace(f"{app_name} v4 Printer Driver", "RustDesk v4 Printer Driver") + lines[i] = line + with open(file_path, "w", encoding="utf-8") as f: + f.writelines(lines) def gen_upgrade_info(): def func(lines, index_start): @@ -542,3 +555,4 @@ def replace_component_guids_in_wxs(): sys.exit(-1) replace_app_name_in_langs(args.app_name) + replace_app_name_in_custom_actions(args.app_name) diff --git a/src/client.rs b/src/client.rs index c056a64f3bc..def76a0e171 100644 --- a/src/client.rs +++ b/src/client.rs @@ -49,6 +49,7 @@ use hbb_common::{ self, Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS, }, + fs::JobType, get_version_number, log, message_proto::{option_message::BoolOption, *}, protobuf::{Message as _, MessageField}, @@ -3297,7 +3298,7 @@ pub enum Data { Close, Login((String, String, String, bool)), Message(Message), - SendFiles((i32, String, String, i32, bool, bool)), + SendFiles((i32, JobType, String, String, i32, bool, bool)), RemoveDirAll((i32, String, bool, bool)), ConfirmDeleteFiles((i32, i32)), SetNoConfirm(i32), @@ -3311,7 +3312,7 @@ pub enum Data { ToggleClipboardFile, NewRDP, SetConfirmOverrideFile((i32, i32, bool, bool, bool)), - AddJob((i32, String, String, i32, bool, bool)), + AddJob((i32, JobType, String, String, i32, bool, bool)), ResumeJob((i32, bool)), RecordScreen(bool), ElevateDirect, diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 88f0b14a5d6..e6b97781810 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -7,6 +7,14 @@ pub trait FileManager: Interface { fs::get_home_as_string() } + fn get_next_job_id(&self) -> i32 { + fs::get_next_job_id() + } + + fn update_next_job_id(&self, id: i32) { + fs::update_next_job_id(id); + } + #[cfg(not(any( target_os = "android", target_os = "ios", @@ -98,6 +106,7 @@ pub trait FileManager: Interface { fn send_files( &self, id: i32, + r#type: i32, path: String, to: String, file_num: i32, @@ -106,6 +115,7 @@ pub trait FileManager: Interface { ) { self.send(Data::SendFiles(( id, + r#type.into(), path, to, file_num, @@ -117,6 +127,7 @@ pub trait FileManager: Interface { fn add_job( &self, id: i32, + r#type: i32, path: String, to: String, file_num: i32, @@ -125,6 +136,7 @@ pub trait FileManager: Interface { ) { self.send(Data::AddJob(( id, + r#type.into(), path, to, file_num, diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 4f084151f78..5f06c23febe 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -46,6 +46,7 @@ use std::{ collections::HashMap, ffi::c_void, num::NonZeroI64, + path::PathBuf, sync::{ atomic::{AtomicUsize, Ordering}, Arc, RwLock, @@ -549,13 +550,20 @@ impl Remote { } allow_err!(peer.send(&msg).await); } - Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { + Data::SendFiles((id, r#type, path, to, file_num, include_hidden, is_remote)) => { log::info!("send files, is remote {}", is_remote); let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); if is_remote { log::debug!("New job {}, write to {} from remote {}", id, to, path); + let to = match r#type { + fs::JobType::Generic => fs::DataSource::FilePath(PathBuf::from(&to)), + fs::JobType::Printer => { + fs::DataSource::MemoryCursor(std::io::Cursor::new(Vec::new())) + } + }; self.write_jobs.push(fs::TransferJob::new_write( id, + r#type, path.clone(), to, file_num, @@ -565,14 +573,15 @@ impl Remote { od, )); allow_err!( - peer.send(&fs::new_send(id, path, file_num, include_hidden)) + peer.send(&fs::new_send(id, r#type, path, file_num, include_hidden)) .await ); } else { match fs::TransferJob::new_read( id, + r#type, to.clone(), - path.clone(), + fs::DataSource::FilePath(PathBuf::from(&path)), file_num, include_hidden, is_remote, @@ -616,7 +625,7 @@ impl Remote { } } } - Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { + Data::AddJob((id, r#type, path, to, file_num, include_hidden, is_remote)) => { let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); if is_remote { log::debug!( @@ -627,8 +636,9 @@ impl Remote { ); let mut job = fs::TransferJob::new_write( id, + r#type, path.clone(), - to, + fs::DataSource::FilePath(PathBuf::from(&to)), file_num, include_hidden, is_remote, @@ -640,8 +650,9 @@ impl Remote { } else { match fs::TransferJob::new_read( id, + r#type, to.clone(), - path.clone(), + fs::DataSource::FilePath(PathBuf::from(&path)), file_num, include_hidden, is_remote, @@ -679,6 +690,7 @@ impl Remote { allow_err!( peer.send(&fs::new_send( id, + fs::JobType::Generic, job.remote.clone(), job.file_num, job.show_hidden @@ -688,17 +700,25 @@ impl Remote { } } else { if let Some(job) = get_job(id, &mut self.read_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_receive( - id, - job.path.to_string_lossy().to_string(), - job.file_num, - job.files.clone(), - job.total_size(), - )) - .await - ); + match &job.data_source { + fs::DataSource::FilePath(p) => { + job.is_last_job = false; + allow_err!( + peer.send(&fs::new_receive( + id, + p.to_string_lossy().to_string(), + job.file_num, + job.files.clone(), + job.total_size(), + )) + .await + ); + } + fs::DataSource::MemoryCursor(_) => { + // unreachable!() + log::error!("Resume job with memory cursor"); + } + } } } } @@ -803,11 +823,10 @@ impl Remote { }); msg_out.set_file_action(file_action); allow_err!(peer.send(&msg_out).await); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + if let Some(job) = fs::remove_job(id, &mut self.write_jobs) { job.remove_download_file(); - fs::remove_job(id, &mut self.write_jobs); } - fs::remove_job(id, &mut self.read_jobs); + let _ = fs::remove_job(id, &mut self.read_jobs); self.remove_jobs.remove(&id); } Data::RemoveDir((id, path)) => { @@ -1402,92 +1421,105 @@ impl Remote { if digest.is_upload { if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { if let Some(file) = job.files().get(digest.file_num as usize) { - let read_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip( - true, - ) - }), - ..Default::default() - }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } else { - self.handler.override_file_confirm( - digest.id, - digest.file_num, - read_path, - true, - digest.is_identical, - ); + if let fs::DataSource::FilePath(p) = &job.data_source { + let read_path = + get_string(&fs::TransferJob::join(p, &file.name)); + let overwrite_strategy = + job.default_overwrite_strategy(); + if let Some(overwrite) = overwrite_strategy { + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip( + true, + ) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + self.handler.override_file_confirm( + digest.id, + digest.file_num, + read_path, + true, + digest.is_identical, + ); + } } } } } else { if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { if let Some(file) = job.files().get(digest.file_num as usize) { - let write_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - match fs::is_write_need_confirmation(&write_path, &digest) { - Ok(res) => match res { - DigestCheckResult::IsSame => { - let req = FileTransferSendConfirmRequest { + if let fs::DataSource::FilePath(p) = &job.data_source { + let write_path = + get_string(&fs::TransferJob::join(p, &file.name)); + let overwrite_strategy = + job.default_overwrite_strategy(); + match fs::is_write_need_confirmation( + &write_path, + &digest, + ) { + Ok(res) => match res { + DigestCheckResult::IsSame => { + let req = FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, union: Some(file_transfer_send_confirm_request::Union::Skip(true)), ..Default::default() }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } - DigestCheckResult::NeedConfirm(digest) => { - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip(true) - }), - ..Default::default() - }; job.confirm(&req); let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); - } else { - self.handler.override_file_confirm( - digest.id, - digest.file_num, - write_path, - false, - digest.is_identical, - ); } - } - DigestCheckResult::NoSuchFile => { - let req = FileTransferSendConfirmRequest { + DigestCheckResult::NeedConfirm(digest) => { + if let Some(overwrite) = overwrite_strategy + { + let req = + FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip(true) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + self.handler.override_file_confirm( + digest.id, + digest.file_num, + write_path, + false, + digest.is_identical, + ); + } + } + DigestCheckResult::NoSuchFile => { + let req = FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), ..Default::default() }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } + }, + Err(err) => { + println!("error receiving digest: {}", err); } - }, - Err(err) => { - println!("error receiving digest: {}", err); } } } @@ -1499,23 +1531,56 @@ impl Remote { if let Err(_err) = job.write(block).await { // to-do: add "skip" for writing job } - self.update_jobs_status(); + if job.r#type == fs::JobType::Generic { + self.update_jobs_status(); + } } } Some(file_response::Union::Done(d)) => { let mut err: Option = None; - if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { + let mut job_type = fs::JobType::Generic; + let mut printer_data = None; + if let Some(job) = fs::remove_job(d.id, &mut self.write_jobs) { job.modify_time(); err = job.job_error(); - fs::remove_job(d.id, &mut self.write_jobs); + job_type = job.r#type; + printer_data = job.get_buf_data(); + } + match job_type { + fs::JobType::Generic => { + self.handle_job_status(d.id, d.file_num, err); + } + fs::JobType::Printer => + { + #[cfg(target_os = "windows")] + if let Some(data) = printer_data { + let printer_name = self + .handler + .printer_names + .write() + .unwrap() + .remove(&d.id); + crate::platform::send_raw_data_to_printer( + printer_name, + data, + ) + .ok(); + } + } } - self.handle_job_status(d.id, d.file_num, err); } Some(file_response::Union::Error(e)) => { - if let Some(_job) = fs::get_job(e.id, &mut self.write_jobs) { - fs::remove_job(e.id, &mut self.write_jobs); + let job_type = fs::remove_job(e.id, &mut self.write_jobs) + .map(|j| j.r#type) + .unwrap_or(fs::JobType::Generic); + match job_type { + fs::JobType::Generic => { + self.handle_job_status(e.id, e.file_num, Some(e.error)); + } + fs::JobType::Printer => { + log::error!("Printer job error: {}", e.error); + } } - self.handle_job_status(e.id, e.file_num, Some(e.error)); } _ => {} } @@ -1739,6 +1804,41 @@ impl Remote { } } Some(message::Union::FileAction(action)) => match action.union { + Some(file_action::Union::Send(_s)) => match _s.file_type.enum_value() { + #[cfg(target_os = "windows")] + Ok(file_transfer_send_request::FileType::Printer) => { + #[cfg(feature = "flutter")] + let action = LocalConfig::get_option( + config::keys::OPTION_PRINTER_INCOMING_JOB_ACTION, + ); + #[cfg(not(feature = "flutter"))] + let action = ""; + if action == "dismiss" { + // Just ignore the incoming print job. + } else { + let id = fs::get_next_job_id(); + #[cfg(feature = "flutter")] + let allow_auto_print = LocalConfig::get_bool_option( + config::keys::OPTION_PRINTER_ALLOW_AUTO_PRINT, + ); + #[cfg(not(feature = "flutter"))] + let allow_auto_print = false; + if allow_auto_print { + let printer_name = if action == "" { + "".to_string() + } else { + LocalConfig::get_option( + config::keys::OPTION_PRINTER_SELECTED_NAME, + ) + }; + self.handler.printer_response(id, _s.path, printer_name); + } else { + self.handler.printer_request(id, _s.path); + } + } + } + _ => {} + }, Some(file_action::Union::SendConfirm(c)) => { if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { job.confirm(&c); @@ -2004,7 +2104,7 @@ impl Remote { async fn handle_cliprdr_msg( &mut self, clip: hbb_common::message_proto::Cliprdr, - peer: &mut Stream, + _peer: &mut Stream, ) { log::debug!("handling cliprdr msg from server peer"); #[cfg(feature = "flutter")] @@ -2074,7 +2174,7 @@ impl Remote { } if let Some(msg) = out_msg { - allow_err!(peer.send(&msg).await); + allow_err!(_peer.send(&msg).await); } } } diff --git a/src/common.rs b/src/common.rs index 83272a71071..26d25f789d7 100644 --- a/src/common.rs +++ b/src/common.rs @@ -139,6 +139,10 @@ pub fn is_support_file_copy_paste_num(ver: i64) -> bool { ver >= hbb_common::get_version_number("1.3.8") } +pub fn is_support_remote_print(ver: &str) -> bool { + hbb_common::get_version_number(ver) >= hbb_common::get_version_number("1.3.9") +} + pub fn is_support_file_paste_if_macos(ver: &str) -> bool { hbb_common::get_version_number(ver) >= hbb_common::get_version_number("1.3.9") } diff --git a/src/core_main.rs b/src/core_main.rs index 4e1fc115955..90c1261bb17 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -196,12 +196,11 @@ pub fn core_main() -> Option> { if config::is_disable_installation() { return None; } - let res = platform::install_me( - "desktopicon startmenu", - "".to_owned(), - true, - args.len() > 1, - ); + #[cfg(not(windows))] + let options = "desktopicon startmenu"; + #[cfg(windows)] + let options = "desktopicon startmenu printer"; + let res = platform::install_me(options, "".to_owned(), true, args.len() > 1); let text = match res { Ok(_) => translate("Installation Successful!".to_string()), Err(err) => { diff --git a/src/flutter.rs b/src/flutter.rs index 0ea552254b4..13c7bdd153d 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1059,6 +1059,14 @@ impl InvokeUiSession for FlutterHandler { fn update_record_status(&self, start: bool) { self.push_event("record_status", &[("start", &start.to_string())], &[]); } + + fn printer_request(&self, id: i32, path: String) { + self.push_event( + "printer_request", + &[("id", json!(id)), ("path", json!(path))], + &[], + ); + } } impl FlutterHandler { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 2e66a11a2bf..84b39dc8543 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -624,7 +624,15 @@ pub fn session_send_files( _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); + session.send_files( + act_id, + fs::JobType::Generic.into(), + path, + to, + file_num, + include_hidden, + is_remote, + ); } } @@ -749,7 +757,15 @@ pub fn session_add_job( is_remote: bool, ) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.add_job(act_id, path, to, file_num, include_hidden, is_remote); + session.add_job( + act_id, + fs::JobType::Generic.into(), + path, + to, + file_num, + include_hidden, + is_remote, + ); } } @@ -1668,6 +1684,17 @@ pub fn session_toggle_virtual_display(session_id: SessionID, index: i32, on: boo } } +pub fn session_printer_response( + session_id: SessionID, + id: i32, + path: String, + printer_name: String, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.printer_response(id, path, printer_name); + } +} + pub fn main_set_home_dir(_home: String) { #[cfg(any(target_os = "android", target_os = "ios"))] { @@ -2362,6 +2389,68 @@ pub fn main_audio_support_loopback() -> SyncReturn { SyncReturn(is_surpport) } +pub fn main_get_printer_names() -> SyncReturn { + #[cfg(target_os = "windows")] + return SyncReturn( + serde_json::to_string(&crate::platform::windows::get_printer_names().unwrap_or_default()) + .unwrap_or_default(), + ); + #[cfg(not(target_os = "windows"))] + return SyncReturn("".to_owned()); +} + +pub fn main_get_common(key: String) -> String { + if key == "is-printer-installed" { + #[cfg(target_os = "windows")] + { + return match remote_printer::is_rd_printer_installed(&get_app_name()) { + Ok(r) => r.to_string(), + Err(e) => e.to_string(), + }; + } + #[cfg(not(target_os = "windows"))] + return false.to_string(); + } else if key == "is-support-printer-driver" { + #[cfg(target_os = "windows")] + return crate::platform::is_win_10_or_greater().to_string(); + #[cfg(not(target_os = "windows"))] + return false.to_string(); + } else if key == "transfer-job-id" { + return hbb_common::fs::get_next_job_id().to_string(); + } else { + "".to_owned() + } +} + +pub fn main_get_common_sync(key: String) -> SyncReturn { + SyncReturn(main_get_common(key)) +} + +pub fn main_set_common(_key: String, _value: String) { + #[cfg(target_os = "windows")] + if _key == "install-printer" && crate::platform::is_win_10_or_greater() { + std::thread::spawn(move || { + let (success, msg) = match remote_printer::install_update_printer(&get_app_name()) { + Ok(_) => (true, "".to_owned()), + Err(e) => { + let err = e.to_string(); + log::error!("Failed to install/update rd printer: {}", &err); + (false, err) + } + }; + let data = HashMap::from([ + ("name", serde_json::json!("install-printer-res")), + ("success", serde_json::json!(success)), + ("msg", serde_json::json!(msg)), + ]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + }); + } +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; diff --git a/src/ipc.rs b/src/ipc.rs index 7e447576b0f..07b0117401d 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -272,6 +272,8 @@ pub enum Data { HwCodecConfig(Option), RemoveTrustedDevices(Vec), ClearTrustedDevices, + #[cfg(all(target_os = "windows", feature = "flutter"))] + PrinterData(Vec), } #[tokio::main(flavor = "current_thread")] @@ -461,7 +463,7 @@ async fn handle(data: Data, stream: &mut Connection) { .lock() .unwrap() .iter() - .filter(|x| x.1 == crate::server::AuthConnType::Remote) + .filter(|x| x.conn_type == crate::server::AuthConnType::Remote) .count(); allow_err!(stream.send(&Data::VideoConnCount(Some(n))).await); } diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 0cd8b3e6c42..8b6cbd0c6fb 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 5c490713032..b618d21f82f 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index b153b035885..e11e7db4a81 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 7a524f21c9e..8120ace84b5 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index dd8d6d10ab9..0297af774b7 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "没有摄像头"), ("d3d_render_tip", "当启用 D3D 渲染时,某些机器可能无法显示远程画面。"), ("Use D3D rendering", "使用 D3D 渲染"), + ("Printer", "打印机"), + ("printer-os-requirement-tip", "打印机的传出功能需要 Windows 10 或更高版本。"), + ("printer-requires-installed-{}-client-tip", "请先安装 {} 客户端。"), + ("printer-{}-not-installed-tip", "未安装 {} 打印机。"), + ("printer-{}-ready-tip", "{} 打印机已安装,您可以使用打印功能了。"), + ("Install {} Printer", "安装 {} 打印机"), + ("Outgoing Print Jobs", "传出的打印任务"), + ("Incomming Print Jobs", "传入的打印任务"), + ("Incoming Print Job", "传入的打印任务"), + ("use-the-default-printer-tip", "使用默认的打印机执行"), + ("use-the-selected-printer-tip", "使用选择的打印机执行"), + ("auto-print-tip", "使用选择的打印机自动执行"), + ("print-incoming-job-confirm-tip", "您收到一个远程打印任务,您想在本地执行它吗?"), + ("remote-printing-disallowed-tile-tip", "不允许远程打印"), + ("remote-printing-disallowed-text-tip", "被控端的权限设置拒绝了远程打印。"), + ("save-settings-tip", "保存设置"), + ("dont-show-again-tip", "不再显示此信息"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index f64a5df2386..6f287a03c7c 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 3f8f14cfb1c..32bd528810b 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index f9f8994ed41..a1c69481a7b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Keine Kameras"), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 505a211c8dc..dc23cd03daa 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 26754a0a58b..04d661ec923 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -241,5 +241,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("upgrade_remote_rustdesk_client_to_{}_tip", "Please upgrade the RustDesk client to version {} or newer on the remote side!"), ("view_camera_unsupported_tip", "The remote device does not support viewing the camera."), ("d3d_render_tip", "When D3D rendering is enabled, the remote control screen may be black on some machines."), + ("printer-requires-installed-{}-client-tip", "In order to use remote printing, {} needs to be installed on this device."), + ("printer-os-requirement-tip", "The printer outgoing function requires Windows 10 or higher."), + ("printer-{}-not-installed-tip", "The {} Printer is not installed."), + ("printer-{}-ready-tip", "The {} Printer is installed and ready to use."), + ("auto-print-tip", "Print automatically using the selected printer."), + ("print-incoming-job-confirm-tip", "You received a print job from remote. Do you want to execute it at your side?"), + ("use-the-default-printer-tip", "Use the default printer"), + ("use-the-selected-printer-tip", "Use the selected printer"), + ("remote-printing-disallowed-tile-tip", "Remote Printing disallowed"), + ("remote-printing-disallowed-text-tip", "The permission settings of the controlled side deny Remote Printing."), + ("save-settings-tip", "Save settings"), + ("dont-show-again-tip", "Don't show this again"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 22a6c73f54c..a92c36449d8 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index d889dba740b..51f1f3043fa 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "No hay cámaras"), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index f4db11cc355..2922711851f 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 5d47f36c7f7..851aa25fdd7 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 9fc72dc753e..948e005e469 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 6c5761437ea..5750768b36e 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Aucune caméra"), ("d3d_render_tip", "Sur certaines machines, l’écran du contrôle à distance peut rester noir lors de l’utilisation du rendu D3D."), ("Use D3D rendering", "Utiliser le rendu D3D"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index b602d91ffc4..5799d0b60a2 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index d9ff9b481aa..46c9795b48e 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 1ea2152b522..8d14cdcd2dd 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f6fac705317..f397442af97 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 58fda3f7ef3..e2b5857e04e 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Nessuna camera"), ("d3d_render_tip", "Quando è abilitato il rendering D3D, in alcuni computer la schermata del telecomando potrebbe essere nera."), ("Use D3D rendering", "Usa rendering D3D"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index bdd9b41b410..a18d6d8e68a 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 08e27f8af93..d104dd452d5 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index b820e2f39d3..82b095e503e 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 97b4a205c32..07bbf2ac951 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 45942447d0d..474a6cbdbbc 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Nav kameru"), ("d3d_render_tip", "Ja ir iespējota D3D renderēšana, dažās ierīcēs tālvadības pults ekrāns var būt melns."), ("Use D3D rendering", "Izmantot D3D renderēšanu"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 73dae60284a..a6d909a9b48 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 7bfd50d54b2..55b44acd6e9 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Geen camera's"), ("d3d_render_tip", "Wanneer D3D-rendering is ingeschakeld kan het externe scherm op sommige apparaten, zwart zijn."), ("Use D3D rendering", "Gebruik D3D-rendering"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 37e6344670b..6c03abdc733 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 6246bce81f3..155c8e16189 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index e7c0ee5d1ab..9d250cd21b4 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 95040f10961..9703a4d1560 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 6c0698d0ab0..c431e91fd2d 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Камера отсутствует"), ("d3d_render_tip", "При включении визуализации D3D на некоторых устройствах удалённый экран может быть чёрным."), ("Use D3D rendering", "Использовать визуализацию D3D"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index f5f57d3d14f..b583655ad9c 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Peruna càmera"), ("d3d_render_tip", "Cando sa renderizatzione D3D est abilitada, s'ischermu de controllu remotu diat pòdere èssere nieddu in unas cantas màchinas"), ("Use D3D rendering", "Imprea sa renderizatzione D3D"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 531ccbf71f2..9f96cb60ec0 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index c3fb9ec4f51..405208f52ec 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 3142b231751..b1467f661bd 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 90007d7eb30..8e469d9be68 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 4493f7ab415..5d3135719fc 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 87ced6bf7d5..d4d1760901e 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index ccf3b9bcd79..064a2f657e6 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index c1b41d2df1c..010bd3c2e44 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index e248898f9e7..1e36b85fdc5 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 4a2647bb166..66ebf00f14d 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "沒有鏡頭"), ("d3d_render_tip", "當啟用 D3D 渲染時,某些機器可能會無法顯示遠端畫面。"), ("Use D3D rendering", "使用 D3D 渲染"), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 578e889e511..475dd071c89 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 61a63f2afc6..e4462749523 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -664,5 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incomming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), ].iter().cloned().collect(); } diff --git a/src/platform/windows.cc b/src/platform/windows.cc index cbf80c49769..682f5bbf37b 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include #include @@ -11,6 +13,7 @@ #include #include #include +#include extern "C" uint32_t get_session_user_info(PWSTR bufin, uint32_t nin, uint32_t id); @@ -859,4 +862,114 @@ extern "C" return isRunning; } -} // end of extern "C" \ No newline at end of file +} // end of extern "C" + +// Remote printing +extern "C" +{ +#pragma comment(lib, "XpsPrint.lib") +#pragma warning(push) +#pragma warning(disable : 4995) + +#define PRINT_XPS_CHECK_HR(hr, msg) \ + if (FAILED(hr)) \ + { \ + _com_error err(hr); \ + flog("%s Error: %s\n", msg, err.ErrorMessage()); \ + return -1; \ + } + + int PrintXPSRawData(LPWSTR printerName, BYTE *rawData, ULONG dataSize) + { + BOOL isCoInitializeOk = FALSE; + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (hr == RPC_E_CHANGED_MODE) + { + hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + } + if (hr == S_OK) + { + isCoInitializeOk = TRUE; + } + std::shared_ptr coInitGuard(nullptr, [isCoInitializeOk](int *) { + if (isCoInitializeOk) CoUninitialize(); + }); + + IXpsOMObjectFactory *xpsFactory = nullptr; + hr = CoCreateInstance( + __uuidof(XpsOMObjectFactory), + nullptr, + CLSCTX_INPROC_SERVER, + __uuidof(IXpsOMObjectFactory), + reinterpret_cast(&xpsFactory)); + PRINT_XPS_CHECK_HR(hr, "Failed to create XPS object factory."); + std::shared_ptr xpsFactoryGuard( + xpsFactory, + [](IXpsOMObjectFactory *xpsFactory) { + xpsFactory->Release(); + }); + + HANDLE completionEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + if (completionEvent == nullptr) + { + flog("Failed to create completion event. Last error: %d\n", GetLastError()); + return -1; + } + std::shared_ptr completionEventGuard( + &completionEvent, + [](HANDLE *completionEvent) { + CloseHandle(*completionEvent); + }); + + IXpsPrintJob *job = nullptr; + IXpsPrintJobStream *jobStream = nullptr; + // `StartXpsPrintJob()` is deprecated, but we still use it for compatibility. + // We may change to use the `Print Document Package API` in the future. + // https://learn.microsoft.com/en-us/windows/win32/printdocs/xpsprint-functions + hr = StartXpsPrintJob( + printerName, + L"Print Job 1", + nullptr, + nullptr, + completionEvent, + nullptr, + 0, + &job, + &jobStream, + nullptr); + PRINT_XPS_CHECK_HR(hr, "Failed to start XPS print job."); + + std::shared_ptr jobStreamGuard(jobStream, [](IXpsPrintJobStream *jobStream) { + jobStream->Release(); + }); + BOOL jobOk = FALSE; + std::shared_ptr jobGuard(job, [&jobOk](IXpsPrintJob* job) { + if (jobOk == FALSE) + { + job->Cancel(); + } + job->Release(); + }); + + DWORD bytesWritten = 0; + hr = jobStream->Write(rawData, dataSize, &bytesWritten); + PRINT_XPS_CHECK_HR(hr, "Failed to write data to print job stream."); + + hr = jobStream->Close(); + PRINT_XPS_CHECK_HR(hr, "Failed to close print job stream."); + + // Wait about 5 minutes for the print job to complete. + DWORD waitMillis = 300 * 1000; + DWORD waitResult = WaitForSingleObject(completionEvent, waitMillis); + if (waitResult != WAIT_OBJECT_0) + { + flog("Wait for print job completion failed. Last error: %d\n", GetLastError()); + return -1; + } + jobOk = TRUE; + + return 0; + } + +#pragma warning(pop) +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 6c0136128a1..ed2b71c5df4 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -21,23 +21,22 @@ use std::{ fs, io::{self, prelude::*}, mem, - os::windows::process::CommandExt, + os::{raw::c_ulong, windows::process::CommandExt}, path::*, ptr::null_mut, sync::{atomic::Ordering, Arc, Mutex}, time::{Duration, Instant}, }; use wallpaper; +#[cfg(not(debug_assertions))] +use winapi::um::libloaderapi::{LoadLibraryExW, LOAD_LIBRARY_SEARCH_USER_DIRS}; use winapi::{ ctypes::c_void, shared::{minwindef::*, ntdef::NULL, windef::*, winerror::*}, um::{ errhandlingapi::GetLastError, handleapi::CloseHandle, - libloaderapi::{ - GetProcAddress, LoadLibraryExA, LoadLibraryExW, LOAD_LIBRARY_SEARCH_SYSTEM32, - LOAD_LIBRARY_SEARCH_USER_DIRS, - }, + libloaderapi::{GetProcAddress, LoadLibraryExA, LOAD_LIBRARY_SEARCH_SYSTEM32}, minwinbase::STILL_ACTIVE, processthreadsapi::{ GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, @@ -54,6 +53,10 @@ use winapi::{ TOKEN_ELEVATION, TOKEN_QUERY, }, winreg::HKEY_CURRENT_USER, + winspool::{ + EnumPrintersW, GetDefaultPrinterW, PRINTER_ENUM_CONNECTIONS, PRINTER_ENUM_LOCAL, + PRINTER_INFO_1W, + }, winuser::*, }, }; @@ -73,6 +76,7 @@ pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW"; const REG_NAME_INSTALL_DESKTOPSHORTCUTS: &str = "DESKTOPSHORTCUTS"; const REG_NAME_INSTALL_STARTMENUSHORTCUTS: &str = "STARTMENUSHORTCUTS"; +const REG_NAME_INSTALL_PRINTER: &str = "PRINTER"; pub fn get_focused_display(displays: Vec) -> Option { unsafe { @@ -1011,6 +1015,10 @@ pub fn get_install_options() -> String { if let Some(start_menu_shortcuts) = start_menu_shortcuts { opts.insert(REG_NAME_INSTALL_STARTMENUSHORTCUTS, start_menu_shortcuts); } + let printer = get_reg_of_hkcr(&subkey, REG_NAME_INSTALL_PRINTER); + if let Some(printer) = printer { + opts.insert(REG_NAME_INSTALL_PRINTER, printer); + } serde_json::to_string(&opts).unwrap_or("{}".to_owned()) } @@ -1136,6 +1144,7 @@ fn get_after_install( exe: &str, reg_value_start_menu_shortcuts: Option, reg_value_desktop_shortcuts: Option, + reg_value_printer: Option, ) -> String { let app_name = crate::get_app_name(); let ext = app_name.to_lowercase(); @@ -1159,12 +1168,20 @@ fn get_after_install( ) }) .unwrap_or_default(); + let reg_printer = reg_value_printer + .map(|v| { + format!( + "reg add HKEY_CLASSES_ROOT\\.{ext} /f /v {REG_NAME_INSTALL_PRINTER} /t REG_SZ /d \"{v}\"" + ) + }) + .unwrap_or_default(); format!(" chcp 65001 reg add HKEY_CLASSES_ROOT\\.{ext} /f {desktop_shortcuts} {start_menu_shortcuts} + {reg_printer} 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 @@ -1249,6 +1266,7 @@ oLink.Save 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 reg_value_printer = "0".to_owned(); let mut shortcuts = Default::default(); if options.contains("desktopicon") { shortcuts = format!( @@ -1268,6 +1286,10 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\" ); reg_value_start_menu_shortcuts = "1".to_owned(); } + let install_printer = options.contains("printer") && crate::platform::is_win_10_or_greater(); + if install_printer { + reg_value_printer = "1".to_owned(); + } let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; let size = meta.len() / 1024; @@ -1338,7 +1360,8 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" after_install = get_after_install( &exe, Some(reg_value_start_menu_shortcuts), - Some(reg_value_desktop_shortcuts) + Some(reg_value_desktop_shortcuts), + Some(reg_value_printer) ), sleep = if debug { "timeout 300" } else { "" }, dels = if debug { "" } else { &dels }, @@ -1346,13 +1369,22 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" import_config = get_import_config(&exe), ); run_cmds(cmds, debug, "install")?; + if install_printer { + allow_err!(remote_printer::install_update_printer( + &crate::get_app_name() + )); + } run_after_run_cmds(silent); Ok(()) } pub fn run_after_install() -> ResultType<()> { let (_, _, _, exe) = get_install_info(); - run_cmds(get_after_install(&exe, None, None), true, "after_install") + run_cmds( + get_after_install(&exe, None, None, None), + true, + "after_install", + ) } pub fn run_before_uninstall() -> ResultType<()> { @@ -1413,6 +1445,9 @@ fn get_uninstall(kill_self: bool) -> String { } pub fn uninstall_me(kill_self: bool) -> ResultType<()> { + if crate::platform::is_win_10_or_greater() { + remote_printer::uninstall_printer(&crate::get_app_name()); + } run_cmds(get_uninstall(kill_self), true, "uninstall") } @@ -1570,9 +1605,18 @@ pub fn bootstrap() -> bool { *config::EXE_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone(); } - set_safe_load_dll() + #[cfg(debug_assertions)] + { + true + } + #[cfg(not(debug_assertions))] + { + // This function will cause `'sciter.dll' was not found neither in PATH nor near the current executable.` when debugging RustDesk. + set_safe_load_dll() + } } +#[cfg(not(debug_assertions))] fn set_safe_load_dll() -> bool { if !unsafe { set_default_dll_directories() } { return false; @@ -1589,6 +1633,7 @@ fn set_safe_load_dll() -> bool { } // https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-setdefaultdlldirectories +#[cfg(not(debug_assertions))] unsafe fn set_default_dll_directories() -> bool { let module = LoadLibraryExW( wide_string("Kernel32.dll").as_ptr(), @@ -2728,3 +2773,119 @@ pub mod reg_display_settings { } } } + +pub fn get_printer_names() -> ResultType> { + let mut needed_bytes = 0; + let mut returned_count = 0; + + unsafe { + // First call to get required buffer size + EnumPrintersW( + PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, + std::ptr::null_mut(), + 1, + std::ptr::null_mut(), + 0, + &mut needed_bytes, + &mut returned_count, + ); + + let mut buffer = vec![0u8; needed_bytes as usize]; + + if EnumPrintersW( + PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, + std::ptr::null_mut(), + 1, + buffer.as_mut_ptr() as *mut _, + needed_bytes, + &mut needed_bytes, + &mut returned_count, + ) == 0 + { + return Err(anyhow!("Failed to enumerate printers")); + } + + let ptr = buffer.as_ptr() as *const PRINTER_INFO_1W; + let printers = std::slice::from_raw_parts(ptr, returned_count as usize); + + Ok(printers + .iter() + .filter_map(|p| { + let name = p.pName; + if !name.is_null() { + let mut len = 0; + while len < 500 { + if name.add(len).is_null() || *name.add(len) == 0 { + break; + } + len += 1; + } + if len > 0 && len < 500 { + Some(String::from_utf16_lossy(std::slice::from_raw_parts( + name, len, + ))) + } else { + None + } + } else { + None + } + }) + .collect()) + } +} + +extern "C" { + fn PrintXPSRawData(printer_name: *const u16, raw_data: *const u8, data_size: c_ulong) -> DWORD; +} + +pub fn send_raw_data_to_printer(printer_name: Option, data: Vec) -> ResultType<()> { + let mut printer_name = printer_name.unwrap_or_default(); + if printer_name.is_empty() { + // use GetDefaultPrinter to get the default printer name + let mut needed_bytes = 0; + unsafe { + GetDefaultPrinterW(std::ptr::null_mut(), &mut needed_bytes); + } + if needed_bytes > 0 { + let mut default_printer_name = vec![0u16; needed_bytes as usize]; + unsafe { + GetDefaultPrinterW( + default_printer_name.as_mut_ptr() as *mut _, + &mut needed_bytes, + ); + } + printer_name = String::from_utf16_lossy(&default_printer_name[..needed_bytes as usize]); + } + } else { + if let Ok(names) = crate::platform::windows::get_printer_names() { + if !names.contains(&printer_name) { + // Don't set the first printer as current printer. + // It may not be the desired printer. + log::error!( + "Printer name \"{}\" not found, ignore the print job", + printer_name + ); + bail!("Printer name \"{}\" not found", &printer_name); + } + } + } + if printer_name.is_empty() { + log::error!("Failed to get printer name"); + return Err(anyhow!("Failed to get printer name")); + } + + let printer_name = wide_string(&printer_name); + unsafe { + let res = PrintXPSRawData( + printer_name.as_ptr(), + data.as_ptr() as *const u8, + data.len() as c_ulong, + ); + if res != 0 { + bail!("Failed to send file to printer, see logs in C:\\Windows\\temp\\test_rustdesk.log for more details."); + } + } + + Ok(()) +} diff --git a/src/server.rs b/src/server.rs index dcb7023f5c4..87e6f390f5f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -70,6 +70,9 @@ mod service; mod video_qos; pub mod video_service; +#[cfg(all(target_os = "windows", feature = "flutter"))] +pub mod printer_service; + pub type Childs = Arc>>; type ConnMap = HashMap; @@ -129,6 +132,20 @@ pub fn new() -> ServerPtr { server.add_service(Box::new(input_service::new_window_focus())); } } + #[cfg(all(target_os = "windows", feature = "flutter"))] + { + match printer_service::init(&crate::get_app_name()) { + Ok(()) => { + log::info!("printer service initialized"); + server.add_service(Box::new(printer_service::new( + printer_service::NAME.to_owned(), + ))); + } + Err(e) => { + log::error!("printer service init failed: {}", e); + } + } + } Arc::new(RwLock::new(server)) } diff --git a/src/server/connection.rs b/src/server/connection.rs index 95f4b88effb..a225acb0281 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -28,7 +28,7 @@ use hbb_common::platform::linux::run_cmds; use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ config::{self, keys, Config, TrustedDevice}, - fs::{self, can_enable_overwrite_detection}, + fs::{self, can_enable_overwrite_detection, JobType}, futures::{SinkExt, StreamExt}, get_time, get_version_number, message_proto::{option_message::BoolOption, permission_info::Permission}, @@ -67,7 +67,7 @@ lazy_static::lazy_static! { static ref LOGIN_FAILURES: [Arc::>>; 2] = 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())); } @@ -245,6 +245,8 @@ pub struct Connection { follow_remote_cursor: bool, follow_remote_window: bool, multi_ui_session: bool, + tx_from_authed: mpsc::UnboundedSender, + printer_data: Vec<(Instant, String, Vec)>, } impl ConnInner { @@ -309,6 +311,7 @@ impl Connection { let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_input, _rx_input) = std_mpsc::channel(); + let (tx_from_authed, mut rx_from_authed) = mpsc::unbounded_channel::(); let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); #[cfg(not(any(target_os = "android", target_os = "ios")))] let (tx_cm_stream_ready, _rx_cm_stream_ready) = mpsc::channel(1); @@ -396,6 +399,8 @@ impl Connection { delayed_read_dir: None, #[cfg(target_os = "macos")] retina: Retina::default(), + tx_from_authed, + printer_data: Vec::new(), }; let addr = hbb_common::try_into_v4(addr); if !conn.on_open(addr).await { @@ -758,6 +763,19 @@ impl Connection { break; } }, + Some(data) = rx_from_authed.recv() => { + match data { + #[cfg(all(target_os = "windows", feature = "flutter"))] + ipc::Data::PrinterData(data) => { + if config::Config::get_bool_option(config::keys::OPTION_ENABLE_REMOTE_PRINTER) { + conn.send_printer_request(data).await; + } else { + conn.send_remote_printing_disallowed().await; + } + } + _ => {} + } + } _ = second_timer.tick() => { #[cfg(windows)] conn.portable_check(); @@ -1104,6 +1122,22 @@ impl Connection { }); } + fn get_files_for_audit(job_type: fs::JobType, mut files: Vec) -> Vec<(String, i64)> { + files + .drain(..) + .map(|f| { + ( + if job_type == fs::JobType::Printer { + "Remote print".to_owned() + } else { + f.name + }, + f.size as _, + ) + }) + .collect() + } + fn post_file_audit( &self, r#type: FileAuditType, @@ -1212,6 +1246,8 @@ impl Connection { self.inner.id(), auth_conn_type, self.session_key(), + self.tx_from_authed.clone(), + self.lr.clone(), )); self.session_last_recv_time = SESSIONS .lock() @@ -2318,7 +2354,15 @@ impl Connection { } } Some(message::Union::FileAction(fa)) => { - if self.file_transfer.is_some() { + let mut handle_fa = self.file_transfer.is_some(); + if !handle_fa { + if let Some(file_action::Union::Send(s)) = fa.union.as_ref() { + if JobType::from_proto(s.file_type) == JobType::Printer { + handle_fa = true; + } + } + } + if handle_fa { if self.delayed_read_dir.is_some() { if let Some(file_action::Union::ReadDir(rd)) = fa.union { self.delayed_read_dir = Some((rd.path, rd.include_hidden)); @@ -2375,10 +2419,32 @@ impl Connection { &self.lr.version, )); let path = s.path.clone(); + let r#type = JobType::from_proto(s.file_type); + let data_source; + match r#type { + JobType::Generic => { + data_source = + fs::DataSource::FilePath(PathBuf::from(&path)); + } + JobType::Printer => { + if let Some(pd) = + self.printer_data.iter().find(|(_, p, _)| *p == path) + { + data_source = fs::DataSource::MemoryCursor( + std::io::Cursor::new(pd.2.clone()), + ); + self.printer_data.retain(|f| f.1 != path); + } else { + // Ignore this message if the printer data is not found + return true; + } + } + }; match fs::TransferJob::new_read( id, + r#type, "".to_string(), - path.clone(), + data_source, s.file_num, s.include_hidden, false, @@ -2390,19 +2456,21 @@ impl Connection { Ok(mut job) => { self.send(fs::new_dir(id, path, job.files().to_vec())) .await; - let mut files = job.files().to_owned(); + let files = job.files().to_owned(); job.is_remote = true; job.conn_id = self.inner.id(); + let job_type = job.r#type; self.read_jobs.push(job); self.file_timer = crate::rustdesk_interval(time::interval(MILLI1)); self.post_file_audit( FileAuditType::RemoteSend, - &s.path, - files - .drain(..) - .map(|f| (f.name, f.size as _)) - .collect(), + if job_type == fs::JobType::Printer { + "Remote print" + } else { + &s.path + }, + Self::get_files_for_audit(job_type, files), json!({}), ); } @@ -2433,11 +2501,7 @@ impl Connection { self.post_file_audit( FileAuditType::RemoteReceive, &r.path, - r.files - .to_vec() - .drain(..) - .map(|f| (f.name, f.size as _)) - .collect(), + Self::get_files_for_audit(fs::JobType::Generic, r.files), json!({}), ); self.file_transferred = true; @@ -2476,13 +2540,12 @@ impl Connection { } Some(file_action::Union::Cancel(c)) => { self.send_fs(ipc::FS::CancelWrite { id: c.id }); - if let Some(job) = fs::get_job_immutable(c.id, &self.read_jobs) { + if let Some(job) = fs::remove_job(c.id, &mut self.read_jobs) { self.send_to_cm(ipc::Data::FileTransferLog(( "transfer".to_string(), - fs::serialize_transfer_job(job, false, true, ""), + fs::serialize_transfer_job(&job, false, true, ""), ))); } - fs::remove_job(c.id, &mut self.read_jobs); } Some(file_action::Union::SendConfirm(r)) => { if let Some(job) = fs::get_job(r.id, &mut self.read_jobs) { @@ -3635,6 +3698,32 @@ impl Connection { fn try_empty_file_clipboard(&mut self) { try_empty_clipboard_files(ClipboardSide::Host, self.inner.id()); } + + #[cfg(all(target_os = "windows", feature = "flutter"))] + async fn send_printer_request(&mut self, data: Vec) { + // This path is only used to identify the printer job. + let path = format!("RustDesk://FsJob//Printer/{}", get_time()); + + let msg = fs::new_send(0, fs::JobType::Printer, path.clone(), 1, false); + self.send(msg).await; + self.printer_data + .retain(|(t, _, _)| t.elapsed().as_secs() < 60); + self.printer_data.push((Instant::now(), path, data)); + } + + #[cfg(all(target_os = "windows", feature = "flutter"))] + async fn send_remote_printing_disallowed(&mut self) { + let mut msg_out = Message::new(); + let res = MessageBox { + msgtype: "custom-nook-nocancel-hasclose".to_owned(), + title: "remote-printing-disallowed-tile-tip".to_owned(), + text: "remote-printing-disallowed-text-tip".to_owned(), + link: "".to_owned(), + ..Default::default() + }; + msg_out.set_message_box(res); + self.send(msg_out).await; + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { @@ -3970,6 +4059,19 @@ fn start_wakelock_thread() -> std::sync::mpsc::Sender<(usize, usize)> { tx } +#[cfg(all(target_os = "windows", feature = "flutter"))] +pub fn on_printer_data(data: Vec) { + crate::server::AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.printer) + .next() + .map(|c| { + c.sender.send(Data::PrinterData(data)).ok(); + }); +} + #[cfg(windows)] pub struct PortableState { pub last_uac: bool, @@ -4103,6 +4205,14 @@ impl Retina { } } +pub struct AuthedConn { + pub conn_id: i32, + pub conn_type: AuthConnType, + pub session_key: SessionKey, + pub sender: mpsc::UnboundedSender, + pub printer: bool, +} + mod raii { // ALIVE_CONNS: all connections, including unauthorized connections // AUTHED_CONNS: all authorized connections @@ -4127,11 +4237,23 @@ mod raii { pub struct AuthedConnID(i32, AuthConnType); impl AuthedConnID { - pub fn new(conn_id: i32, conn_type: AuthConnType, session_key: SessionKey) -> Self { - AUTHED_CONNS - .lock() - .unwrap() - .push((conn_id, conn_type, session_key)); + pub fn new( + conn_id: i32, + conn_type: AuthConnType, + session_key: SessionKey, + sender: mpsc::UnboundedSender, + lr: LoginRequest, + ) -> Self { + let printer = conn_type == crate::server::AuthConnType::Remote + && crate::is_support_remote_print(&lr.version) + && lr.my_platform == whoami::Platform::Windows.to_string(); + AUTHED_CONNS.lock().unwrap().push(AuthedConn { + conn_id, + conn_type, + session_key, + sender, + printer, + }); Self::check_wake_lock(); use std::sync::Once; static _ONCE: Once = Once::new(); @@ -4153,7 +4275,7 @@ mod raii { .lock() .unwrap() .iter() - .filter(|c| c.1 == AuthConnType::Remote) + .filter(|c| c.conn_type == AuthConnType::Remote) .count(); allow_err!(WAKELOCK_SENDER .lock() @@ -4166,7 +4288,7 @@ mod raii { .lock() .unwrap() .iter() - .filter(|c| c.1 != AuthConnType::PortForward) + .filter(|c| c.conn_type != AuthConnType::PortForward) .count() } @@ -4179,16 +4301,16 @@ mod raii { .lock() .unwrap() .iter() - .any(|c| c.0 == conn_id && c.1 == AuthConnType::Remote); + .any(|c| c.conn_id == conn_id && c.conn_type == 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; // 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 && c.1 == AuthConnType::Remote); + let another_remote = AUTHED_CONNS.lock().unwrap().iter().any(|c| { + c.conn_id != conn_id + && c.session_key == key + && c.conn_type == AuthConnType::Remote + }); if is_remote || !another_remote { lock.remove(&key); log::info!("remove session"); @@ -4256,12 +4378,12 @@ mod raii { .unwrap() .on_connection_close(self.0); } - AUTHED_CONNS.lock().unwrap().retain(|c| c.0 != self.0); + AUTHED_CONNS.lock().unwrap().retain(|c| c.conn_id != self.0); let remote_count = AUTHED_CONNS .lock() .unwrap() .iter() - .filter(|c| c.1 == AuthConnType::Remote) + .filter(|c| c.conn_type == AuthConnType::Remote) .count(); if remote_count == 0 { #[cfg(any(target_os = "windows", target_os = "linux"))] diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 257f4d71b4f..06391ad7cd5 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -505,7 +505,7 @@ pub fn try_stop_record_cursor_pos() { .lock() .unwrap() .iter() - .filter(|c| c.1 == AuthConnType::Remote) + .filter(|c| c.conn_type == AuthConnType::Remote) .count(); if remote_count > 0 { return; diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index b8cba71dd7c..4a4eaaad10f 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -812,7 +812,7 @@ pub mod client { .lock() .unwrap() .iter() - .filter(|c| c.1 == crate::server::AuthConnType::Remote) + .filter(|c| c.conn_type == crate::server::AuthConnType::Remote) .count(); stream.send(&Data::DataPortableService(ConnCount(Some(remote_count)))).await.ok(); } diff --git a/src/server/printer_service.rs b/src/server/printer_service.rs new file mode 100644 index 00000000000..268429a585d --- /dev/null +++ b/src/server/printer_service.rs @@ -0,0 +1,163 @@ +use super::service::{EmptyExtraFieldService, GenericService, Service}; +use hbb_common::{bail, dlopen::symbor::Library, log, ResultType}; +use std::{ + sync::{Arc, Mutex}, + thread, + time::Duration, +}; + +pub const NAME: &'static str = "remote-printer"; + +const LIB_NAME_PRINTER_DRIVER_ADAPTER: &str = "printer_driver_adapter"; + +// Return 0 if success, otherwise return error code. +pub type Init = fn(tag_name: *const i8) -> i32; +pub type Uninit = fn(); +// dur_mills: Get the file generated in the last `dur_mills` milliseconds. +// data: The raw prn data, xps format. +// data_len: The length of the raw prn data. +pub type GetPrnData = fn(dur_mills: u32, data: *mut *mut i8, data_len: *mut u32); + +macro_rules! make_lib_wrapper { + ($($field:ident : $tp:ty),+) => { + struct LibWrapper { + _lib: Option, + $($field: Option<$tp>),+ + } + + impl LibWrapper { + fn new() -> Self { + let lib_name = match get_lib_name() { + Ok(name) => name, + Err(e) => { + log::warn!("Failed to get lib name, {}", e); + return Self { + _lib: None, + $( $field: None ),+ + }; + } + }; + let lib = match Library::open(&lib_name) { + Ok(lib) => Some(lib), + Err(e) => { + log::warn!("Failed to load library {}, {}", &lib_name, e); + None + } + }; + + $(let $field = if let Some(lib) = &lib { + match unsafe { lib.symbol::<$tp>(stringify!($field)) } { + Ok(m) => { + log::info!("method found {}", stringify!($field)); + Some(*m) + }, + Err(e) => { + log::warn!("Failed to load func {}, {}", stringify!($field), e); + None + } + } + } else { + None + };)+ + + Self { + _lib: lib, + $( $field ),+ + } + } + } + + impl Default for LibWrapper { + fn default() -> Self { + Self::new() + } + } + } +} + +make_lib_wrapper!( + init: Init, + uninit: Uninit, + get_prn_data: GetPrnData +); + +lazy_static::lazy_static! { + static ref LIB_WRAPPER: Arc> = Default::default(); +} + +fn get_lib_name() -> ResultType { + let exe_file = std::env::current_exe()?; + if let Some(cur_dir) = exe_file.parent() { + let dll_name = format!("{}.dll", LIB_NAME_PRINTER_DRIVER_ADAPTER); + let full_path = cur_dir.join(dll_name); + if !full_path.exists() { + bail!("{} not found", full_path.to_string_lossy().as_ref()); + } else { + Ok(full_path.to_string_lossy().into_owned()) + } + } else { + bail!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ); + } +} + +pub fn init(app_name: &str) -> ResultType<()> { + let lib_wrapper = LIB_WRAPPER.lock().unwrap(); + let Some(fn_init) = lib_wrapper.init.as_ref() else { + bail!("Failed to load func init"); + }; + + let tag_name = std::ffi::CString::new(app_name)?; + let ret = fn_init(tag_name.as_ptr()); + if ret != 0 { + bail!("Failed to init printer driver"); + } + Ok(()) +} + +pub fn uninit() { + let lib_wrapper = LIB_WRAPPER.lock().unwrap(); + if let Some(fn_uninit) = lib_wrapper.uninit.as_ref() { + fn_uninit(); + } +} + +fn get_prn_data(dur_mills: u32) -> ResultType> { + let lib_wrapper = LIB_WRAPPER.lock().unwrap(); + if let Some(fn_get_prn_data) = lib_wrapper.get_prn_data.as_ref() { + let mut data = std::ptr::null_mut(); + let mut data_len = 0u32; + fn_get_prn_data(dur_mills, &mut data, &mut data_len); + if data.is_null() || data_len == 0 { + return Ok(Vec::new()); + } + let bytes = + Vec::from(unsafe { std::slice::from_raw_parts(data as *const u8, data_len as usize) }); + unsafe { + hbb_common::libc::free(data as *mut std::ffi::c_void); + } + Ok(bytes) + } else { + bail!("Failed to load func get_prn_file"); + } +} + +pub fn new(name: String) -> GenericService { + let svc = EmptyExtraFieldService::new(name, false); + GenericService::run(&svc.clone(), run); + svc.sp +} + +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + while sp.ok() { + let bytes = get_prn_data(1000)?; + if !bytes.is_empty() { + log::info!("Got prn data, data len: {}", bytes.len()); + crate::server::on_printer_data(bytes); + } + thread::sleep(Duration::from_millis(300)); + } + Ok(()) +} diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index 6c741b31f4b..0b60cf748a0 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -69,8 +69,6 @@ function getExt(name) { return ""; } -var jobIdCounter = 1; - class JobTable: Reactor.Component { this var jobs = []; this var job_map = {}; @@ -126,8 +124,7 @@ class JobTable: Reactor.Component { } if (!to) return; to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path); - var id = jobIdCounter; - jobIdCounter += 1; + var id = handler.get_next_job_id(); this.jobs.push({ type: "transfer", id: id, path: path, to: to, include_hidden: show_hidden, @@ -135,7 +132,7 @@ class JobTable: Reactor.Component { is_last: false }); this.job_map[id] = this.jobs[this.jobs.length - 1]; - handler.send_files(id, path, to, 0, show_hidden, is_remote); + handler.send_files(id, 0, path, to, 0, show_hidden, is_remote); var self = this; self.timer(30ms, function() { self.update(); }); } @@ -147,8 +144,8 @@ class JobTable: Reactor.Component { is_remote: is_remote, is_last: true, file_num: file_num }; this.jobs.push(job); this.job_map[id] = this.jobs[this.jobs.length - 1]; - jobIdCounter = id + 1; - handler.add_job(id, path, to, file_num, show_hidden, is_remote); + handler.update_next_job_id(id + 1); + handler.add_job(id, 0, path, to, file_num, show_hidden, is_remote); stdout.println(JSON.stringify(job)); } @@ -162,16 +159,14 @@ class JobTable: Reactor.Component { } function addDelDir(path, is_remote) { - var id = jobIdCounter; - jobIdCounter += 1; + var id = handler.get_next_job_id(); this.jobs.push({ type: "del-dir", id: id, path: path, is_remote: is_remote }); this.job_map[id] = this.jobs[this.jobs.length - 1]; this.update(); } function addDelFile(path, is_remote) { - var id = jobIdCounter; - jobIdCounter += 1; + var id = handler.get_next_job_id(); this.jobs.push({ type: "del-file", id: id, path: path, is_remote: is_remote }); this.job_map[id] = this.jobs[this.jobs.length - 1]; this.update(); @@ -552,9 +547,9 @@ class FolderView : Reactor.Component { return; } var path = me.joinPath(name); - handler.create_dir(jobIdCounter, path, me.is_remote); - create_dir_jobs[jobIdCounter] = { is_remote: me.is_remote, path: path }; - jobIdCounter += 1; + var id = handler.get_next_job_id(); + handler.create_dir(id, path, me.is_remote); + create_dir_jobs[id] = { is_remote: me.is_remote, path: path }; }); } diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index 34f6b04432d..da557e31222 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -317,6 +317,9 @@ class MsgboxComponent: Reactor.Component { if (this.type == "multiple-sessions-nocancel") { values.sid = (this.$$(select))[0].value; } + if (this.type == "remote-printer-selector") { + values.name = (this.$$(select))[0].value; + } return values; } diff --git a/src/ui/printer.tis b/src/ui/printer.tis new file mode 100644 index 00000000000..c2848260191 --- /dev/null +++ b/src/ui/printer.tis @@ -0,0 +1,41 @@ +include "sciter:reactor.tis"; + +handler.printerRequest = function(id, path) { + show_printer_selector(id, path); +}; + +function show_printer_selector(id, path) +{ + var names = handler.get_printer_names(); + msgbox("remote-printer-selector", "Incoming Print Job", , "", function(res=null) { + if (res && res.name) { + handler.on_printer_selected(id, path, res.name); + } + }, 180); +} + +class PrinterComponent extends Reactor.Component { + this var names = []; + this var jobTip = translate("print-incoming-job-confirm-tip"); + + function this(params) { + if (params && params.names) { + this.names = params.names; + } + } + + function render() { + return
    +
    {translate("print-incoming-job-confirm-tip")}
    +
    +
    + +
    +
    +
    ; + } +} diff --git a/src/ui/remote.html b/src/ui/remote.html index d58c3449bd8..70e909d17d2 100644 --- a/src/ui/remote.html +++ b/src/ui/remote.html @@ -15,6 +15,7 @@ include "port_forward.tis"; include "grid.tis"; include "header.tis"; + include "printer.tis";
    diff --git a/src/ui/remote.rs b/src/ui/remote.rs index d4d89b64e23..fffc51b86f1 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -379,6 +379,10 @@ impl InvokeUiSession for SciterHandler { fn update_record_status(&self, start: bool) { self.call("updateRecordStatus", &make_args!(start)); } + + fn printer_request(&self, id: i32, path: String) { + self.call("printerRequest", &make_args!(id, path)); + } } pub struct SciterSession(Session); @@ -491,6 +495,8 @@ impl sciter::EventHandler for SciterSession { fn get_chatbox(); fn get_icon(); fn get_home_dir(); + fn get_next_job_id(); + fn update_next_job_id(i32); fn read_dir(String, bool); fn remove_dir(i32, String, bool); fn create_dir(i32, String, bool); @@ -502,8 +508,8 @@ impl sciter::EventHandler for SciterSession { fn confirm_delete_files(i32, i32); fn set_no_confirm(i32); fn cancel_job(i32); - fn send_files(i32, String, String, i32, bool, bool); - fn add_job(i32, String, String, i32, bool, bool); + fn send_files(i32, i32, String, String, i32, bool, bool); + fn add_job(i32, i32, String, String, i32, bool, bool); fn resume_job(i32, bool); fn get_platform(bool); fn get_path_sep(bool); @@ -541,6 +547,8 @@ impl sciter::EventHandler for SciterSession { fn set_selected_windows_session_id(String); fn is_recording(); fn has_file_clipboard(); + fn get_printer_names(); + fn on_printer_selected(i32, String, String); } } @@ -842,6 +850,22 @@ impl SciterSession { fn version_cmp(&self, v1: String, v2: String) -> i32 { (hbb_common::get_version_number(&v1) - hbb_common::get_version_number(&v2)) as i32 } + + fn get_printer_names(&self) -> Value { + #[cfg(target_os = "windows")] + let printer_names = crate::platform::windows::get_printer_names().unwrap_or_default(); + #[cfg(not(target_os = "windows"))] + let printer_names: Vec = vec![]; + let mut v = Value::array(0); + for name in printer_names { + v.push(name); + } + v + } + + fn on_printer_selected(&self, id: i32, path: String, printer_name: String) { + self.printer_response(id, path, printer_name); + } } pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index b36c37be79d..1da297a4ef8 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -744,6 +744,8 @@ async fn handle_fs( tx: &UnboundedSender, tx_log: Option<&UnboundedSender>, ) { + use std::path::PathBuf; + use hbb_common::fs::serialize_transfer_job; match fs { @@ -785,8 +787,9 @@ async fn handle_fs( // dummy remote, show_hidden, is_remote let mut job = fs::TransferJob::new_write( id, + fs::JobType::Generic, "".to_string(), - path, + fs::DataSource::FilePath(PathBuf::from(&path)), file_num, false, false, @@ -805,27 +808,24 @@ async fn handle_fs( write_jobs.push(job); } ipc::FS::CancelWrite { id } => { - if let Some(job) = fs::get_job(id, write_jobs) { + if let Some(job) = fs::remove_job(id, write_jobs) { job.remove_download_file(); tx_log.map(|tx: &UnboundedSender| { - tx.send(serialize_transfer_job(job, false, true, "")) + tx.send(serialize_transfer_job(&job, false, true, "")) }); - fs::remove_job(id, write_jobs); } } ipc::FS::WriteDone { id, file_num } => { - if let Some(job) = fs::get_job(id, write_jobs) { + if let Some(job) = fs::remove_job(id, write_jobs) { job.modify_time(); send_raw(fs::new_done(id, file_num), tx); - tx_log.map(|tx| tx.send(serialize_transfer_job(job, true, false, ""))); - fs::remove_job(id, write_jobs); + tx_log.map(|tx| tx.send(serialize_transfer_job(&job, true, false, ""))); } } ipc::FS::WriteError { id, file_num, err } => { - if let Some(job) = fs::get_job(id, write_jobs) { - tx_log.map(|tx| tx.send(serialize_transfer_job(job, false, false, &err))); + if let Some(job) = fs::remove_job(id, write_jobs) { + tx_log.map(|tx| tx.send(serialize_transfer_job(&job, false, false, &err))); send_raw(fs::new_error(job.id(), err, file_num), tx); - fs::remove_job(job.id(), write_jobs); } } ipc::FS::WriteBlock { @@ -871,32 +871,34 @@ async fn handle_fs( ..Default::default() }; if let Some(file) = job.files().get(file_num as usize) { - let path = get_string(&job.join(&file.name)); - match is_write_need_confirmation(&path, &digest) { - Ok(digest_result) => { - match digest_result { - DigestCheckResult::IsSame => { - req.set_skip(true); - let msg_out = new_send_confirm(req); - send_raw(msg_out, &tx); - } - DigestCheckResult::NeedConfirm(mut digest) => { - // upload to server, but server has the same file, request - digest.is_upload = is_upload; - let mut msg_out = Message::new(); - let mut fr = FileResponse::new(); - fr.set_digest(digest); - msg_out.set_file_response(fr); - send_raw(msg_out, &tx); - } - DigestCheckResult::NoSuchFile => { - let msg_out = new_send_confirm(req); - send_raw(msg_out, &tx); + if let fs::DataSource::FilePath(p) = &job.data_source { + let path = get_string(&fs::TransferJob::join(p, &file.name)); + match is_write_need_confirmation(&path, &digest) { + Ok(digest_result) => { + match digest_result { + DigestCheckResult::IsSame => { + req.set_skip(true); + let msg_out = new_send_confirm(req); + send_raw(msg_out, &tx); + } + DigestCheckResult::NeedConfirm(mut digest) => { + // upload to server, but server has the same file, request + digest.is_upload = is_upload; + let mut msg_out = Message::new(); + let mut fr = FileResponse::new(); + fr.set_digest(digest); + msg_out.set_file_response(fr); + send_raw(msg_out, &tx); + } + DigestCheckResult::NoSuchFile => { + let msg_out = new_send_confirm(req); + send_raw(msg_out, &tx); + } } } - } - Err(err) => { - send_raw(fs::new_error(id, err, file_num), &tx); + Err(err) => { + send_raw(fs::new_error(id, err, file_num), &tx); + } } } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index d4d9e10220b..fb06493fba3 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -58,6 +58,7 @@ pub struct Session { pub server_clipboard_enabled: Arc>, pub last_change_display: Arc>, pub connection_round_state: Arc>, + pub printer_names: Arc>>, } #[derive(Clone)] @@ -1505,6 +1506,20 @@ impl Session { pub fn get_conn_token(&self) -> Option { self.lc.read().unwrap().get_conn_token() } + + pub fn printer_response(&self, id: i32, path: String, printer_name: String) { + self.printer_names.write().unwrap().insert(id, printer_name); + let to = std::env::temp_dir().join(format!("rustdesk_printer_{id}")); + self.send(Data::SendFiles(( + id, + hbb_common::fs::JobType::Printer, + path, + to.to_string_lossy().to_string(), + 0, + false, + true, + ))); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { @@ -1570,6 +1585,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn is_multi_ui_session(&self) -> bool; fn update_record_status(&self, start: bool); fn update_empty_dirs(&self, _res: ReadEmptyDirsResponse) {} + fn printer_request(&self, id: i32, path: String); } impl Deref for Session { From ee2478168c30bbaddc8008ab3be5e830e5b75782 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 28 Mar 2025 10:36:42 +0800 Subject: [PATCH 165/506] fix: remote printer (#11270) * fix: remote printer, log Signed-off-by: fufesou * fix: remote printer, avoid double sign Signed-off-by: fufesou * Spawn a new thread to handle the print job. Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/client/io_loop.rs | 41 +++++++++++++++++++++++++--------------- src/server/connection.rs | 10 ++++++---- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 5f06c23febe..bca9badd4f3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1550,21 +1550,32 @@ impl Remote { fs::JobType::Generic => { self.handle_job_status(d.id, d.file_num, err); } - fs::JobType::Printer => - { - #[cfg(target_os = "windows")] - if let Some(data) = printer_data { - let printer_name = self - .handler - .printer_names - .write() - .unwrap() - .remove(&d.id); - crate::platform::send_raw_data_to_printer( - printer_name, - data, - ) - .ok(); + fs::JobType::Printer => { + if let Some(err) = err { + log::error!("Received printer job failed, error {err}"); + } else { + log::info!( + "Received printer job done, data len: {:?}", + printer_data.as_ref().map(|d| d.len()).unwrap_or(0) + ); + #[cfg(target_os = "windows")] + if let Some(data) = printer_data { + let printer_name = self + .handler + .printer_names + .write() + .unwrap() + .remove(&d.id); + // Spawn a new thread to handle the print job. + // Or print job will block the ui thread. + std::thread::spawn(move || { + crate::platform::send_raw_data_to_printer( + printer_name, + data, + ) + .ok(); + }); + } } } } diff --git a/src/server/connection.rs b/src/server/connection.rs index a225acb0281..24038effda0 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2427,13 +2427,15 @@ impl Connection { fs::DataSource::FilePath(PathBuf::from(&path)); } JobType::Printer => { - if let Some(pd) = - self.printer_data.iter().find(|(_, p, _)| *p == path) + if let Some((_, _, data)) = self + .printer_data + .iter() + .position(|(_, p, _)| *p == path) + .map(|index| self.printer_data.remove(index)) { data_source = fs::DataSource::MemoryCursor( - std::io::Cursor::new(pd.2.clone()), + std::io::Cursor::new(data), ); - self.printer_data.retain(|f| f.1 != path); } else { // Ignore this message if the printer data is not found return true; From 4b14f8613461315720e445443a65fdc89b680fd9 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 28 Mar 2025 10:56:19 +0800 Subject: [PATCH 166/506] refact: remote printer, log (#11271) Signed-off-by: fufesou --- src/client/io_loop.rs | 13 ++++++++----- src/platform/windows.rs | 10 ++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index bca9badd4f3..31810644ec5 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1569,11 +1569,14 @@ impl Remote { // Spawn a new thread to handle the print job. // Or print job will block the ui thread. std::thread::spawn(move || { - crate::platform::send_raw_data_to_printer( - printer_name, - data, - ) - .ok(); + if let Err(e) = + crate::platform::send_raw_data_to_printer( + printer_name, + data, + ) + { + log::error!("Print job error: {}", e); + } }); } } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index ed2b71c5df4..2c89b49fbaa 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -2862,19 +2862,15 @@ pub fn send_raw_data_to_printer(printer_name: Option, data: Vec) -> if !names.contains(&printer_name) { // Don't set the first printer as current printer. // It may not be the desired printer. - log::error!( - "Printer name \"{}\" not found, ignore the print job", - printer_name - ); bail!("Printer name \"{}\" not found", &printer_name); } } } if printer_name.is_empty() { - log::error!("Failed to get printer name"); return Err(anyhow!("Failed to get printer name")); } + log::info!("Sending data to printer: {}", &printer_name); let printer_name = wide_string(&printer_name); unsafe { let res = PrintXPSRawData( @@ -2883,7 +2879,9 @@ pub fn send_raw_data_to_printer(printer_name: Option, data: Vec) -> data.len() as c_ulong, ); if res != 0 { - bail!("Failed to send file to printer, see logs in C:\\Windows\\temp\\test_rustdesk.log for more details."); + bail!("Failed to send data to the printer, see logs in C:\\Windows\\temp\\test_rustdesk.log for more details."); + } else { + log::info!("Successfully sent data to the printer"); } } From 23e70c0fd16aaa3ca812615ddada35cd0a2484fe Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 28 Mar 2025 21:58:46 +0800 Subject: [PATCH 167/506] refact: remote printer, adapter dll, free data ptr (#11279) Signed-off-by: fufesou --- libs/hbb_common | 2 +- src/client/io_loop.rs | 12 +++++++++--- src/server/printer_service.rs | 9 +++++---- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/libs/hbb_common b/libs/hbb_common index 9ede5d49f65..81b932b7bfa 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 9ede5d49f65b8c513fe613dbfea2daf7694cba3e +Subproject commit 81b932b7bfa2ff8bc60189625fd6538db2fa9ea1 diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 31810644ec5..8d858e5df97 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1544,7 +1544,13 @@ impl Remote { job.modify_time(); err = job.job_error(); job_type = job.r#type; - printer_data = job.get_buf_data(); + printer_data = match job.get_buf_data().await { + Ok(d) => d, + Err(e) => { + log::error!("Failed to get the printer data: {}", e); + None + } + }; } match job_type { fs::JobType::Generic => { @@ -1552,10 +1558,10 @@ impl Remote { } fs::JobType::Printer => { if let Some(err) = err { - log::error!("Received printer job failed, error {err}"); + log::error!("Receive print job failed, error {err}"); } else { log::info!( - "Received printer job done, data len: {:?}", + "Receive print job done, data len: {:?}", printer_data.as_ref().map(|d| d.len()).unwrap_or(0) ); #[cfg(target_os = "windows")] diff --git a/src/server/printer_service.rs b/src/server/printer_service.rs index 268429a585d..1bf944b9d06 100644 --- a/src/server/printer_service.rs +++ b/src/server/printer_service.rs @@ -17,6 +17,8 @@ pub type Uninit = fn(); // data: The raw prn data, xps format. // data_len: The length of the raw prn data. pub type GetPrnData = fn(dur_mills: u32, data: *mut *mut i8, data_len: *mut u32); +// Free the prn data allocated by GetPrnData(). +pub type FreePrnData = fn(data: *mut i8); macro_rules! make_lib_wrapper { ($($field:ident : $tp:ty),+) => { @@ -78,7 +80,8 @@ macro_rules! make_lib_wrapper { make_lib_wrapper!( init: Init, uninit: Uninit, - get_prn_data: GetPrnData + get_prn_data: GetPrnData, + free_prn_data: FreePrnData ); lazy_static::lazy_static! { @@ -135,9 +138,7 @@ fn get_prn_data(dur_mills: u32) -> ResultType> { } let bytes = Vec::from(unsafe { std::slice::from_raw_parts(data as *const u8, data_len as usize) }); - unsafe { - hbb_common::libc::free(data as *mut std::ffi::c_void); - } + lib_wrapper.free_prn_data.map(|f| f(data)); Ok(bytes) } else { bail!("Failed to load func get_prn_file"); From 5f3b9803730a8df2ffb57b21e4310530089caa5a Mon Sep 17 00:00:00 2001 From: Lynilia <89228568+Lynilia@users.noreply.github.com> Date: Sat, 29 Mar 2025 14:55:28 +0100 Subject: [PATCH 168/506] Update fr.rs (#11266) --- src/lang/fr.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 5750768b36e..d599e653555 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -664,22 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Aucune caméra"), ("d3d_render_tip", "Sur certaines machines, l’écran du contrôle à distance peut rester noir lors de l’utilisation du rendu D3D."), ("Use D3D rendering", "Utiliser le rendu D3D"), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), + ("Printer", "Imprimante"), + ("printer-os-requirement-tip", "La fonction d’impression sortante nécessite Windows 10 ou une version ultérieure."), + ("printer-requires-installed-{}-client-tip", "{} doit être installé sur cet appareil avant de pouvoir utiliser l’impression à distance."), + ("printer-{}-not-installed-tip", "L’imprimante {} n’est pas installée."), + ("printer-{}-ready-tip", "L’imprimante {} est installée et opérationnelle."), + ("Install {} Printer", "Installer l’imprimante {}"), + ("Outgoing Print Jobs", "Impressions sortantes"), + ("Incomming Print Jobs", "Impressions entrantes"), + ("Incoming Print Job", "Impression entrante"), + ("use-the-default-printer-tip", "Utiliser l’imprimante par défaut"), + ("use-the-selected-printer-tip", "Utiliser l’imprimante sélectionnée"), + ("auto-print-tip", "Imprimer automatiquement en utilisant l’imprimante sélectionnée."), + ("print-incoming-job-confirm-tip", "L’appareil distant vous a envoyé une impression ; voulez-vous l’exécuter de votre côté ?"), + ("remote-printing-disallowed-tile-tip", "Impression à distance non autorisée"), + ("remote-printing-disallowed-text-tip", "Les paramètres de l’appareil contrôlé n’autorisent pas l’impression à distance."), + ("save-settings-tip", "Enregistrer les paramètres"), + ("dont-show-again-tip", "Ne plus afficher"), ].iter().cloned().collect(); } From ea74ed12b8628a326e1f521f534ed028f1e92e93 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sun, 30 Mar 2025 08:45:17 +0200 Subject: [PATCH 169/506] Update de.rs (#11269) --- src/lang/de.rs | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index a1c69481a7b..aaee6afecc1 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -662,24 +662,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("view_camera_unsupported_tip", "Das entfernte Gerät kann die Kamera nicht anzeigen."), ("Enable camera", "Kamera zulassen"), ("No cameras", "Keine Kameras"), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), + ("d3d_render_tip", "Wenn das D3D-Rendering aktiviert ist, kann der entfernte Bildschirm auf manchen Rechnern schwarz sein."), + ("Use D3D rendering", "D3D-Rendering verwenden"), + ("Printer", "Drucker"), + ("printer-os-requirement-tip", "Für die Funktion des Druckerausgangs ist Windows 10 oder höher erforderlich."), + ("printer-requires-installed-{}-client-tip", "Um den entfernten Druck nutzen zu können, muss {} auf diesem Gerät installiert sein."), + ("printer-{}-not-installed-tip", "Der Drucker {} ist nicht installiert."), + ("printer-{}-ready-tip", "Der Drucker {} ist installiert und einsatzbereit."), + ("Install {} Printer", "Drucker {} installieren"), + ("Outgoing Print Jobs", "Ausgehende Druckaufträge"), + ("Incomming Print Jobs", "Eingehende Druckaufträge"), + ("Incoming Print Job", "Eingehender Druckauftrag"), + ("use-the-default-printer-tip", "Standarddrucker verwenden"), + ("use-the-selected-printer-tip", "Ausgewählten Drucker verwenden"), + ("auto-print-tip", "Automatischer Druck mit dem ausgewählten Drucker."), + ("print-incoming-job-confirm-tip", "Sie haben einen Druckauftrag aus der Ferne erhalten. Möchten Sie ihn bei sich selbst ausführen?"), + ("remote-printing-disallowed-tile-tip", "Entferntes Drucken nicht erlaubt"), + ("remote-printing-disallowed-text-tip", "Die Berechtigungseinstellungen der kontrollierten Seite verweigern den entfernten Druck."), + ("save-settings-tip", "Einstellungen speichern"), + ("dont-show-again-tip", "Nicht mehr anzeigen"), ].iter().cloned().collect(); } From adf83a1b259ab63d64a1673fa650f22acdaf09a0 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Mon, 31 Mar 2025 07:45:59 +0200 Subject: [PATCH 170/506] Italian language update (#11278) --- src/lang/it.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index e2b5857e04e..524c8625550 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -614,7 +614,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("texture_render_tip", "Usa il rendering texture per rendere le immagini più fluide. Se riscontri problemi di rendering prova a disabilitare questa opzione."), ("Use texture rendering", "Usa rendering texture"), ("Floating window", "Finestra galleggiante"), - ("floating_window_tip", "It helps to keep RustDesk background service"), + ("floating_window_tip", "Aiuta a mantenere il servizio Rustdesk in background."), ("Keep screen on", "Mantieni schermo acceso"), ("Never", "Mai"), ("During controlled", "Durante il controllo"), @@ -664,22 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Nessuna camera"), ("d3d_render_tip", "Quando è abilitato il rendering D3D, in alcuni computer la schermata del telecomando potrebbe essere nera."), ("Use D3D rendering", "Usa rendering D3D"), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), + ("Printer", "Stampante"), + ("printer-os-requirement-tip", "La funzione della stampante richiede Windows 10 o superiore."), + ("printer-requires-installed-{}-client-tip", "Per usare la stampa remota, {} è necessario installare il programma nel dispositivo."), + ("printer-{}-not-installed-tip", "La stampante {} non è installata."), + ("printer-{}-ready-tip", "La stampante {} è installata e pronta all'uso."), + ("Install {} Printer", "Installa la stampante {}"), + ("Outgoing Print Jobs", "Lavori di stampa in uscita"), + ("Incomming Print Jobs", "Lavori di stampa in entrata"), + ("Incoming Print Job", "Lavoro di stampa in entrata"), + ("use-the-default-printer-tip", "Usa la stampante predefinita"), + ("use-the-selected-printer-tip", "Usa la stampante selezionata"), + ("auto-print-tip", "Stampa usando automaticamente la stampante selezionata."), + ("print-incoming-job-confirm-tip", "Hai ricevuto un lavoro di stampa da remoto. Vuoi eseguirlo sul desktop?"), + ("remote-printing-disallowed-tile-tip", "Stampa remota disabilitata"), + ("remote-printing-disallowed-text-tip", "Le impostazioni di autorizzazione del lato controllato negano la stampa remota."), + ("save-settings-tip", "Salva impostazioni"), + ("dont-show-again-tip", "Non visualizzare più questo messaggio"), ].iter().cloned().collect(); } From f32988b4542a3f70105f7cb2a702a3c2fe43340c Mon Sep 17 00:00:00 2001 From: Mahdi Rahimi <31624047+mahdirahimi1999@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:16:15 +0330 Subject: [PATCH 171/506] Updated Persian translations in fa.rs (#11280) --- src/lang/fa.rs | 86 +++++++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 948e005e469..51bc3f4efbb 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -637,49 +637,49 @@ 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", ""), - ("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", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), - ("Accessible devices", ""), + ("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", "کلیپ‌بورد همگام‌سازی شده است"), + ("Update client clipboard", "به‌روزرسانی کلیپ‌بورد کاربر"), + ("Untagged", "بدون برچسب"), + ("new-version-of-{}-tip", "نسخه جدید {} در دسترس است"), + ("Accessible devices", "دستگاه‌های در دسترس"), ("View camera", "نمایش دوربین"), - ("upgrade_remote_rustdesk_client_to_{}_tip", "لطفاً مشتری RustDesk را به نسخه {} یا جدیدتر در سمت راه دور ارتقا دهید!"), - ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "لطفاً RustDesk را به نسخه {} یا جدیدتر در سمت راه دور ارتقا دهید"), + ("view_camera_unsupported_tip", "دوربین در این دستگاه پشتیبانی نمی‌شود"), + ("Enable camera", "فعال کردن دوربین"), + ("No cameras", "هیچ دوربینی یافت نشد"), + ("d3d_render_tip", "فعال کردن رندر D3D برای عملکرد بهتر"), + ("Use D3D rendering", "استفاده از رندر D3D"), + ("Printer", "چاپگر"), + ("printer-os-requirement-tip", "سیستم‌عامل شما باید از چاپ از راه دور پشتیبانی کند"), + ("printer-requires-installed-{}-client-tip", "برای استفاده از چاپگر، کلاینت {} باید نصب باشد"), + ("printer-{}-not-installed-tip", "چاپگر {} نصب نشده است"), + ("printer-{}-ready-tip", "چاپگر {} آماده است"), + ("Install {} Printer", "نصب چاپگر {}"), + ("Outgoing Print Jobs", "وظایف چاپ خروجی"), + ("Incomming Print Jobs", "وظایف چاپ ورودی"), + ("Incoming Print Job", "وظیفه چاپ ورودی"), + ("use-the-default-printer-tip", "از چاپگر پیش‌فرض استفاده کنید"), + ("use-the-selected-printer-tip", "از چاپگر انتخاب‌شده استفاده کنید"), + ("auto-print-tip", "چاپ خودکار فعال است"), + ("print-incoming-job-confirm-tip", "آیا می‌خواهید کار چاپ ورودی را تأیید کنید"), + ("remote-printing-disallowed-tile-tip", "چاپ از راه دور غیرفعال است"), + ("remote-printing-disallowed-text-tip", "شما مجوز لازم برای چاپ از راه دور را ندارید"), + ("save-settings-tip", "تنظیمات را ذخیره کنید"), + ("dont-show-again-tip", "دیگر نمایش داده نشود"), ].iter().cloned().collect(); } From a2725df7cd654a22f0d0e99d8a77489b1e590231 Mon Sep 17 00:00:00 2001 From: Mahdi Rahimi <31624047+mahdirahimi1999@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:16:25 +0330 Subject: [PATCH 172/506] Update Arabic translation in ar.rs (#11281) --- src/lang/ar.rs | 304 ++++++++++++++++++++++++------------------------- 1 file changed, 152 insertions(+), 152 deletions(-) diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 8b6cbd0c6fb..1112bea777e 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -528,158 +528,158 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), - ("Accessible devices", ""), + ("Change Color", "تغيير اللون"), + ("Primary Color", "اللون الأساسي"), + ("HSV Color", "اللون بنظام HSV"), + ("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", "ترقية خادم RustDesk Pro إلى {}"), + ("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", "يجب تكوين SELinux بشكل صحيح لضمان التشغيل السلس."), + ("Change view", "تغيير العرض"), + ("Big tiles", "بلاط كبير"), + ("Small tiles", "بلاط صغير"), + ("List", "قائمة"), + ("Virtual display", "الشاشة الافتراضية"), + ("Plug out all", "فصل الكل"), + ("True color (4:4:4)", "اللون الحقيقي (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", "لا يدعم IDD في Windows 10 الإصدار 2004 أو أقدم."), + ("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.", "يجب أن يتكون رمز التحقق بالبريد الإلكتروني من 6 أحرف."), + ("2FA code must be 6 digits.", "يجب أن يتكون رمز التحقق الثنائي من 6 أرقام."), + ("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", "مسح تحديد الشاشة Wayland"), + ("clear_Wayland_screen_selection_tip", "مسح اختيار الشاشة Wayland الحالي."), + ("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", "أبدًا"), + ("During controlled", "أثناء التحكم"), + ("During service is on", "أثناء تشغيل الخدمة"), + ("Capture screen using DirectX", "التقاط الشاشة باستخدام DirectX"), + ("Back", "رجوع"), + ("Apps", "التطبيقات"), + ("Volume up", "زيادة الصوت"), + ("Volume down", "خفض الصوت"), + ("Power", "الطاقة"), + ("Telegram bot", "بوت تيليجرام"), + ("enable-bot-tip", "تمكين البوت للتفاعل مع RustDesk"), + ("enable-bot-desc", "يمكنك استخدام بوت تيليجرام للتحكم في الجلسات."), + ("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", "تعيين الرقم السري"), + ("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", "تمت مزامنة الحافظة"), + ("Update client clipboard", "تحديث حافظة العميل"), + ("Untagged", "غير موسوم"), + ("new-version-of-{}-tip", "تحديث جديد متاح لـ {}"), + ("Accessible devices", "الأجهزة القابلة للوصول"), ("View camera", "عرض الكاميرا"), - ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "ترقية عميل RustDesk البعيد إلى {}"), + ("view_camera_unsupported_tip", "عرض الكاميرا غير مدعوم في هذا الجهاز"), + ("Enable camera", "تمكين الكاميرا"), + ("No cameras", "لا توجد كاميرات"), + ("d3d_render_tip", "تمكين العرض باستخدام D3D"), + ("Use D3D rendering", "استخدام عرض D3D"), + ("Printer", "الطابعة"), + ("printer-os-requirement-tip", "يتطلب تثبيت الطابعة على النظام."), + ("printer-requires-installed-{}-client-tip", "الطابعة تتطلب عميل {} المثبت."), + ("printer-{}-not-installed-tip", "الطابعة {} غير مثبتة"), + ("printer-{}-ready-tip", "الطابعة {} جاهزة"), + ("Install {} Printer", "تثبيت طابعة {}"), + ("Outgoing Print Jobs", "وظائف الطباعة الصادرة"), + ("Incomming Print Jobs", "وظائف الطباعة الواردة"), + ("Incoming Print Job", "وظيفة طباعة واردة"), + ("use-the-default-printer-tip", "استخدم الطابعة الافتراضية"), + ("use-the-selected-printer-tip", "استخدم الطابعة المحددة"), + ("auto-print-tip", "تمكين الطباعة التلقائية"), + ("print-incoming-job-confirm-tip", "هل أنت متأكد من طباعة هذه الوظيفة؟"), + ("remote-printing-disallowed-tile-tip", "الطباعة عن بُعد غير مسموح بها"), + ("remote-printing-disallowed-text-tip", "الطباعة عن بُعد غير مسموح بها على هذا الجهاز"), + ("save-settings-tip", "حفظ الإعدادات"), + ("dont-show-again-tip", "لا تظهر هذا مرة أخرى"), ].iter().cloned().collect(); } From 3dbe27ea57429cf2b57cbae3b894a9f9a88ff8b5 Mon Sep 17 00:00:00 2001 From: Kleofass <4000163+Kleofass@users.noreply.github.com> Date: Mon, 31 Mar 2025 08:46:38 +0300 Subject: [PATCH 173/506] Update lv.rs (#11298) --- src/lang/lv.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 474a6cbdbbc..8601f48fdd0 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -664,22 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Nav kameru"), ("d3d_render_tip", "Ja ir iespējota D3D renderēšana, dažās ierīcēs tālvadības pults ekrāns var būt melns."), ("Use D3D rendering", "Izmantot D3D renderēšanu"), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), + ("Printer", "Printeris"), + ("printer-os-requirement-tip", "Printera izejošajai funkcijai nepieciešama operētājsistēma Windows 10 vai jaunāka versija."), + ("printer-requires-installed-{}-client-tip", "Lai izmantotu attālo drukāšanu, šajā ierīcē ir jāinstalē {}."), + ("printer-{}-not-installed-tip", "Printeris {} nav instalēts."), + ("printer-{}-ready-tip", "Printeris {} ir instalēts un gatavs lietošanai."), + ("Install {} Printer", "Instalēt {} printeri"), + ("Outgoing Print Jobs", "Izejošie drukas darbi"), + ("Incomming Print Jobs", "Ienākošie drukas darbi"), + ("Incoming Print Job", "Ienākošais drukas darbs"), + ("use-the-default-printer-tip", "Izmantot noklusējuma printeri"), + ("use-the-selected-printer-tip", "Izmantot atlasīto printeri"), + ("auto-print-tip", "Drukājiet automātiski, izmantojot atlasīto printeri."), + ("print-incoming-job-confirm-tip", "Jūs saņēmāt drukas darbu no attālās ierīces. Vai vēlaties to izpildīt savā pusē?"), + ("remote-printing-disallowed-tile-tip", "Attālā drukāšana ir aizliegta"), + ("remote-printing-disallowed-text-tip", "Kontrolētās puses atļauju iestatījumi liedz attālo drukāšanu."), + ("save-settings-tip", "Saglabāt iestatījumus"), + ("dont-show-again-tip", "Nerādīt šo vēlreiz"), ].iter().cloned().collect(); } From ca1b35440b1696754d4f4925643f3ebf4e764592 Mon Sep 17 00:00:00 2001 From: solokot Date: Tue, 1 Apr 2025 16:57:12 +0300 Subject: [PATCH 174/506] Update ru.rs (#11306) --- src/lang/ru.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index c431e91fd2d..d9322bfe12e 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -664,22 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Камера отсутствует"), ("d3d_render_tip", "При включении визуализации D3D на некоторых устройствах удалённый экран может быть чёрным."), ("Use D3D rendering", "Использовать визуализацию D3D"), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), + ("Printer", "Принтер"), + ("printer-os-requirement-tip", "Для работы функции исходящей связи с принтером требуется Windows 10 или более поздней версии."), + ("printer-requires-installed-{}-client-tip", "Чтобы использовать удалённую печать, {} должен быть установлен на этом устройстве."), + ("printer-{}-not-installed-tip", "Принтер {} не установлен."), + ("printer-{}-ready-tip", "Принтер {} установлен и готов к использованию."), + ("Install {} Printer", "Установить принтер {}"), + ("Outgoing Print Jobs", "Исходящее задание печати"), + ("Incomming Print Jobs", "Входящее задание печати"), + ("Incoming Print Job", "Входящее задание печати"), + ("use-the-default-printer-tip", "Использовать принтер по умолчанию"), + ("use-the-selected-printer-tip", "Использовать выбранный принтер"), + ("auto-print-tip", "Автоматически выполнять печать на выбранном принтере."), + ("print-incoming-job-confirm-tip", "Получено задание на печать с удалённого устройства. Выполнить его локально?"), + ("remote-printing-disallowed-tile-tip", "Удалённая печать запрещена"), + ("remote-printing-disallowed-text-tip", "Настройки разрешений на управляемой стороне запрещают удалённую печать."), + ("save-settings-tip", "Сохранить настройки"), + ("dont-show-again-tip", "Больше не показывать"), ].iter().cloned().collect(); } From 11d3ea5f24f81ac571190c70cc9a8737a2d10a66 Mon Sep 17 00:00:00 2001 From: asereze Date: Tue, 1 Apr 2025 15:57:27 +0200 Subject: [PATCH 175/506] Update sc.rs (#11309) --- src/lang/sc.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lang/sc.rs b/src/lang/sc.rs index b583655ad9c..a8acc190964 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -664,22 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "Peruna càmera"), ("d3d_render_tip", "Cando sa renderizatzione D3D est abilitada, s'ischermu de controllu remotu diat pòdere èssere nieddu in unas cantas màchinas"), ("Use D3D rendering", "Imprea sa renderizatzione D3D"), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), + ("Printer", "Imprentadora"), + ("printer-os-requirement-tip", "Pro pòdere impreare s'imprentadora in seddida bisòngiat a installare {} in custu dispositivu."), + ("printer-requires-installed-{}-client-tip", "Pro sa funtzionalidade de imprenta in essida b'at bisòngiu de Window 10 o prus nou."), + ("printer-{}-not-installed-tip", "S'imprentadora {} no est installada"), + ("printer-{}-ready-tip", "S'imprentadora {} est installada e pronta pro s'impreu."), + ("Install {} Printer", "Installa s'imprentadora {}"), + ("Outgoing Print Jobs", "Traballos de imprenta in essida"), + ("Incomming Print Jobs", "Traballos de imprenta in intrada"), + ("Incoming Print Job", "Traballu de imprenta in intrada"), + ("use-the-default-printer-tip", "Imprea s'imprentadora predefinida"), + ("use-the-selected-printer-tip", "Imprea s'imprentadora seletzionada"), + ("auto-print-tip", "Imprenta in automàticu impreende s'imprentadora seletzionada."), + ("print-incoming-job-confirm-tip", "As retzidu unu traballu de imprenta dae remotu. Lu boles esecutare dae s'ala tua?"), + ("remote-printing-disallowed-tile-tip", "Impreanta remota disabilitada"), + ("remote-printing-disallowed-text-tip", "Sas impostatziones de sos permissos de s'ala controllada negant s'imprenta remota."), + ("save-settings-tip", "Sarva sas impostatziones"), + ("dont-show-again-tip", "Non mustres prus custu messàgiu"), ].iter().cloned().collect(); } From c19f33a1378255c2afe1653ddc5e786a9a6eebe3 Mon Sep 17 00:00:00 2001 From: XLion Date: Wed, 2 Apr 2025 17:12:10 +0800 Subject: [PATCH 176/506] Update tw.rs (#11325) --- src/lang/tw.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 66ebf00f14d..cecdf1d05fa 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -664,22 +664,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "沒有鏡頭"), ("d3d_render_tip", "當啟用 D3D 渲染時,某些機器可能會無法顯示遠端畫面。"), ("Use D3D rendering", "使用 D3D 渲染"), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), + ("Printer", "印表機"), + ("printer-os-requirement-tip", "印表機的傳出功能需要 Windows 10 或更高版本。"), + ("printer-requires-installed-{}-client-tip", "為了使用遠端列印功能,請安裝 {} 到此設備。"), + ("printer-{}-not-installed-tip", "{} 印表機未安裝。"), + ("printer-{}-ready-tip", "{} 印表機已安裝,您可以使用列印功能了。"), + ("Install {} Printer", "安裝 {} 印表機"), + ("Outgoing Print Jobs", "傳出的列印任務"), + ("Incomming Print Jobs", "傳入的列印任務"), + ("Incoming Print Job", "傳入的列印任務"), + ("use-the-default-printer-tip", "使用預設的印表機"), + ("use-the-selected-printer-tip", "使用選取的印表機"), + ("auto-print-tip", "使用選取的印表機自動執行"), + ("print-incoming-job-confirm-tip", "您收到一個遠端列印任務,您想在本地執行它嗎?"), + ("remote-printing-disallowed-tile-tip", "不允許遠端列印"), + ("remote-printing-disallowed-text-tip", "被控端的權限設置拒絕了遠端列印。"), + ("save-settings-tip", "儲存設定"), + ("dont-show-again-tip", "不再顯示此訊息"), ].iter().cloned().collect(); } From d808bb29476a67173bb7f08c979e1989f89f9e3e Mon Sep 17 00:00:00 2001 From: Marcos Rodrigo Ladeia Date: Thu, 3 Apr 2025 04:16:00 -0300 Subject: [PATCH 177/506] Update Portuguese (Brazil) Translations (#11322) * Update ptbr.rs * Update ptbr.rs "web_id_input_tip" --- src/lang/ptbr.rs | 104 +++++++++++++++++++++++------------------------ 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 9d250cd21b4..4cb7ca7d8dd 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -230,7 +230,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Nome de usuário requerido"), ("Password missed", "Senha requerida"), ("Wrong credentials", "Nome de usuário ou senha incorretos"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "O código de verificação está incorreto ou expirou"), ("Edit Tag", "Editar Tag"), ("Forget Password", "Esquecer Senha"), ("Favorites", "Favoritos"), @@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Proporção"), ("Image Quality", "Qualidade de Imagem"), ("Scroll Style", "Estilo de Rolagem"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Mostrar Barra de Ferramentas"), + ("Hide Toolbar", "Ocultar Barra de Ferramentas"), ("Direct Connection", "Conexão Direta"), ("Relay Connection", "Conexão via Relay"), ("Secure Connection", "Conexão Segura"), @@ -359,12 +359,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Dispositivo de entrada de áudio"), ("Use IP Whitelisting", "Utilizar lista de IPs confiáveis"), ("Network", "Rede"), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), + ("Pin Toolbar", "Fixar Barra de Ferramentas"), + ("Unpin Toolbar", "Desafixar Barra de Ferramentas"), ("Recording", "Gravando"), ("Directory", "Diretório"), ("Automatically record incoming sessions", "Gravar automaticamente sessões de entrada"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "Gravar automaticamente sessões de saída"), ("Change", "Alterar"), ("Start session recording", "Iniciar gravação da sessão"), ("Stop session recording", "Parar gravação da sessão"), @@ -534,7 +534,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Installation Successful!", "Instalação bem-sucedida!"), ("Installation failed!", "A instalação falhou!"), ("Reverse mouse wheel", "Inverter rolagem do mouse"), - ("{} sessions", ""), + ("{} sessions", "{} sessões"), ("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."), @@ -547,7 +547,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("Filter by intersection", "Filtrar por interseção"), ("Remove wallpaper during incoming sessions", "Remover papel de parede durante sessão remota"), ("Test", "Teste"), ("display_is_plugged_out_msg", "A tela está desconectada. Mudando para a principal."), @@ -565,8 +565,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("privacy_mode_impl_mag_tip", "Modo 1"), + ("privacy_mode_impl_virtual_display_tip", "Modo 2"), ("Enter privacy mode", "Entrar no modo privado"), ("Exit privacy mode", "Sair do modo privado"), ("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."), @@ -589,10 +589,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("Personal", "Pessoal"), ("Owner", "Proprietário"), ("Set shared password", "Definir senha compartilhada"), - ("Exist in", ""), + ("Exist in", "Existe em"), ("Read-only", "Apenas leitura"), ("Read/Write", "Leitura/escrita"), ("Full Control", "Controle total"), @@ -603,14 +603,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("default_proxy_tip", "O protocolo e a porta padrão são Socks5 e 1080"), ("no_audio_input_device_tip", "Nenhum dispositivo de entrada de áudio encontrado"), - ("Incoming", ""), - ("Outgoing", ""), + ("Incoming", "Entrada"), + ("Outgoing", "Saída"), ("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", ""), + ("android_new_voice_call_tip", "Uma nova solicitação de chamada de voz foi recebida. Se você aceitar, o áudio será alternado para comunicação por voz."), ("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"), @@ -624,7 +624,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Apps", "Apps"), ("Volume up", "Aumentar volume"), ("Volume down", "Diminuir volume"), - ("Power", ""), + ("Power", "Energia"), ("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"), @@ -645,41 +645,41 @@ 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", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), - ("Accessible devices", ""), - ("View camera", "Visualizar Câmera"), + ("one-way-file-transfer-tip", "A transferência de arquivos unidirecional está ativada no dispositivo controlado."), + ("Authentication Required", "Autenticação necessária"), + ("Authenticate", "Autenticar"), + ("web_id_input_tip", "Você pode inserir um ID no mesmo servidor; o acesso direto por IP não é suportado no cliente web.\nSe desejar acessar um dispositivo em outro servidor, por favor, adicione o endereço do servidor (@?key=), por exemplo,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe desejar acessar um dispositivo em um servidor público, por favor, insira \"@public\", a chave não é necessária para servidores públicos."), + ("Download", "Baixar"), + ("Upload folder", "Carregar pasta"), + ("Upload files", "Carregar arquivos"), + ("Clipboard is synchronized", "A área de transferência está sincronizada"), + ("Update client clipboard", "Atualizar a área de transferência do cliente"), + ("Untagged", "Sem etiqueta"), + ("new-version-of-{}-tip", "Uma nova versão de {} está disponível"), + ("Accessible devices", "Dispositivos acessíveis"), + ("View camera", "Visualizar câmera"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualize o cliente RustDesk para a versão {} ou superior no lado remoto."), - ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), + ("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."), + ("Enable camera", "Ativar câmera"), + ("No cameras", "Sem câmeras"), + ("d3d_render_tip", "Em algumas máquinas, a tela do controle remoto pode ficar preta ao usar a renderização D3D."), + ("Use D3D rendering", "Usar renderização D3D"), + ("Printer", "Impressora"), + ("printer-os-requirement-tip", "A função de impressão de saída requer Windows 10 ou superior."), + ("printer-requires-installed-{}-client-tip", "{} deve ser instalado neste dispositivo antes que você possa usar a impressão remota."), + ("printer-{}-not-installed-tip", "A impressora {} não está instalada."), + ("printer-{}-ready-tip", "A impressora {} está instalada e operacional."), + ("Install {} Printer", "Instalar impressora {}"), + ("Outgoing Print Jobs", "Impressões de saída"), + ("Incomming Print Jobs", "Impressões recebidas"), + ("Incoming Print Job", "Trabalho de impressão recebido"), + ("use-the-default-printer-tip", "Usar impressora padrão"), + ("use-the-selected-printer-tip", "Usar impressora selecionada"), + ("auto-print-tip", "Imprimir automaticamente usando a impressora selecionada."), + ("print-incoming-job-confirm-tip", "O dispositivo remoto enviou uma impressão; deseja executá-la no seu lado?"), + ("remote-printing-disallowed-tile-tip", "Impressão remota não permitida"), + ("remote-printing-disallowed-text-tip", "As configurações do dispositivo controlado não permitem impressão remota."), + ("save-settings-tip", "Salvar configurações"), + ("dont-show-again-tip", "Não mostrar novamente"), ].iter().cloned().collect(); } From 9ddeab9be23013a4eb4839727cc138f209db8e92 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 5 Apr 2025 08:45:01 +0800 Subject: [PATCH 178/506] fix: vcpkg, cmake, compatibility 3.5 (#11356) Signed-off-by: fufesou --- .../0003-upgrade-cmake-3.14.patch | 10 +++ res/vcpkg/mfx-dispatch/fix-pkgconf.patch | 39 +++++++++++ .../mfx-dispatch/fix-unresolved-symbol.patch | 66 +++++++++++++++++++ res/vcpkg/mfx-dispatch/portfile.cmake | 40 +++++++++++ res/vcpkg/mfx-dispatch/vcpkg.json | 16 +++++ vcpkg.json | 16 +++-- 6 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 res/vcpkg/mfx-dispatch/0003-upgrade-cmake-3.14.patch create mode 100644 res/vcpkg/mfx-dispatch/fix-pkgconf.patch create mode 100644 res/vcpkg/mfx-dispatch/fix-unresolved-symbol.patch create mode 100644 res/vcpkg/mfx-dispatch/portfile.cmake create mode 100644 res/vcpkg/mfx-dispatch/vcpkg.json diff --git a/res/vcpkg/mfx-dispatch/0003-upgrade-cmake-3.14.patch b/res/vcpkg/mfx-dispatch/0003-upgrade-cmake-3.14.patch new file mode 100644 index 00000000000..676c0dd7a93 --- /dev/null +++ b/res/vcpkg/mfx-dispatch/0003-upgrade-cmake-3.14.patch @@ -0,0 +1,10 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index a8a3288..7d01d97 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -1,4 +1,4 @@ +-cmake_minimum_required(VERSION 2.6) ++cmake_minimum_required(VERSION 3.14) + + project( libmfx ) + diff --git a/res/vcpkg/mfx-dispatch/fix-pkgconf.patch b/res/vcpkg/mfx-dispatch/fix-pkgconf.patch new file mode 100644 index 00000000000..c0310e12a9c --- /dev/null +++ b/res/vcpkg/mfx-dispatch/fix-pkgconf.patch @@ -0,0 +1,39 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 9446bc4..a8a3288 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -3,16 +3,7 @@ cmake_minimum_required(VERSION 2.6) + project( libmfx ) + + # FIXME Adds support for using system/other install of intel media sdk +-find_path ( INTELMEDIASDK_PATH mfx/mfxvideo.h +- HINTS "${CMAKE_SOURCE_DIR}" +-) +- +-if (INTELMEDIASDK_PATH_NOTFOUND) +- message( FATAL_ERROR "Intel MEDIA SDK include not found" ) +-else (INTELMEDIASDK_PATH_NOTFOUND) +- message(STATUS "Intel Media SDK is here: ${INTELMEDIASDK_PATH}") +-endif (INTELMEDIASDK_PATH_NOTFOUND) +- ++set(INTELMEDIASDK_PATH "${CMAKE_CURRENT_LIST_DIR}") + + set(SOURCES + src/main.cpp +diff --git a/libmfx.pc.cmake b/libmfx.pc.cmake +index fabb541..5d248fe 100644 +--- a/libmfx.pc.cmake ++++ b/libmfx.pc.cmake +@@ -6,9 +6,9 @@ Requires.private: + Name: libmfx + Description: Intel Media SDK Dispatched static library +-Version: 2013 ++Version: 1.35 + Requires: + Requires.private: + Conflicts: +-Libs: -L${libdir} -lsupc++ ${libdir}/libmfx.lib ++Libs: -L${libdir} -llibmfx + Libs.private: +-Cflags: -I${includedir} -I@INTELMEDIASDK_PATH@ ++Cflags: -I${includedir} diff --git a/res/vcpkg/mfx-dispatch/fix-unresolved-symbol.patch b/res/vcpkg/mfx-dispatch/fix-unresolved-symbol.patch new file mode 100644 index 00000000000..96d9e6d90aa --- /dev/null +++ b/res/vcpkg/mfx-dispatch/fix-unresolved-symbol.patch @@ -0,0 +1,66 @@ +Subject: [PATCH] fix for vcpkg +fix missing mfx_driver_store_loader related symbols +--- +Index: CMakeLists.txt +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/CMakeLists.txt b/CMakeLists.txt +--- a/CMakeLists.txt (revision 7e4d221c36c630c1250b23a5dfa15657bc04c10c) ++++ b/CMakeLists.txt (revision 5ebef171699530ca01594a5cef10a68811f4d105) +@@ -40,6 +39,7 @@ + src/mfx_load_plugin.cpp + src/mfx_plugin_hive.cpp + src/mfx_win_reg_key.cpp ++ src/mfx_driver_store_loader.cpp + ) + endif (CMAKE_SYSTEM_NAME MATCHES "Windows") + +@@ -56,6 +56,12 @@ + configure_file (${CMAKE_SOURCE_DIR}/libmfx.pc.cmake ${CMAKE_BINARY_DIR}/libmfx.pc @ONLY) + + add_library( mfx STATIC ${SOURCES} ) ++ ++if (CMAKE_SYSTEM_NAME MATCHES "Windows") ++ set_target_properties(mfx ++ PROPERTIES PREFIX lib) ++endif (CMAKE_SYSTEM_NAME MATCHES "Windows") ++ + install (DIRECTORY ${CMAKE_SOURCE_DIR}/mfx DESTINATION ${CMAKE_INSTALL_PREFIX}/include FILES_MATCHING PATTERN "*.h") + install (FILES ${CMAKE_BINARY_DIR}/libmfx.pc DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/pkgconfig) + install (TARGETS mfx ARCHIVE DESTINATION ${CMAKE_INSTALL_PREFIX}/lib) +Index: libmfx.pc.cmake +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/libmfx.pc.cmake b/libmfx.pc.cmake +--- a/libmfx.pc.cmake (revision 7e4d221c36c630c1250b23a5dfa15657bc04c10c) ++++ b/libmfx.pc.cmake (revision 388559e9e8234eb0989e1598a9beea4035a04132) +@@ -9,6 +9,6 @@ + Requires: + Requires.private: + Conflicts: +-Libs: -L${libdir} -lsupc++ ${libdir}/libmfx.a ++Libs: -L${libdir} -lsupc++ ${libdir}/libmfx.lib + Libs.private: + Cflags: -I${includedir} -I@INTELMEDIASDK_PATH@ +Index: src/mfx_driver_store_loader.cpp +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/src/mfx_driver_store_loader.cpp b/src/mfx_driver_store_loader.cpp +--- a/src/mfx_driver_store_loader.cpp (revision 388559e9e8234eb0989e1598a9beea4035a04132) ++++ b/src/mfx_driver_store_loader.cpp (revision 5ebef171699530ca01594a5cef10a68811f4d105) +@@ -24,6 +24,9 @@ + #include "mfx_dispatcher_log.h" + #include "mfx_load_dll.h" + ++#pragma comment(lib, "Ole32.lib") ++#pragma comment(lib, "Advapi32.lib") ++ + namespace MFX + { + diff --git a/res/vcpkg/mfx-dispatch/portfile.cmake b/res/vcpkg/mfx-dispatch/portfile.cmake new file mode 100644 index 00000000000..cb2ad7e7ced --- /dev/null +++ b/res/vcpkg/mfx-dispatch/portfile.cmake @@ -0,0 +1,40 @@ +vcpkg_download_distfile( + MISSING_CSTDINT_IMPORT_PATCH + URLS https://github.com/lu-zero/mfx_dispatch/commit/d6241243f85a0d947bdfe813006686a930edef24.patch?full_index=1 + FILENAME fix-missing-cstdint-import-d6241243f85a0d947bdfe813006686a930edef24.patch + SHA512 5d2ffc4ec2ba0e5859d01d2e072f75436ebc3e62e0f6580b5bb8b9f82fe588e7558a46a1fdfa0297a782c0eeb8f50322258d0dd9e41d927cc9be496727b61e44 +) + +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO lu-zero/mfx_dispatch + REF "${VERSION}" + SHA512 12517338342d3e653043a57e290eb9cffd190aede0c3a3948956f1c7f12f0ea859361cf3e534ab066b96b1c211f68409c67ef21fd6d76b68cc31daef541941b0 + HEAD_REF master + PATCHES + fix-unresolved-symbol.patch + fix-pkgconf.patch + 0003-upgrade-cmake-3.14.patch + ${MISSING_CSTDINT_IMPORT_PATCH} +) + +if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) + vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + ) + vcpkg_cmake_install() + vcpkg_copy_pdbs() +else() + if(VCPKG_TARGET_IS_MINGW) + vcpkg_check_linkage(ONLY_STATIC_LIBRARY) + endif() + vcpkg_configure_make( + SOURCE_PATH "${SOURCE_PATH}" + AUTOCONFIG + ) + vcpkg_install_make() +endif() +vcpkg_fixup_pkgconfig() + +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE") diff --git a/res/vcpkg/mfx-dispatch/vcpkg.json b/res/vcpkg/mfx-dispatch/vcpkg.json new file mode 100644 index 00000000000..e8374718812 --- /dev/null +++ b/res/vcpkg/mfx-dispatch/vcpkg.json @@ -0,0 +1,16 @@ +{ + "name": "mfx-dispatch", + "version": "1.35.1", + "port-version": 5, + "description": "Open source Intel media sdk dispatcher", + "homepage": "https://github.com/lu-zero/mfx_dispatch", + "license": "BSD-3-Clause", + "supports": "((x86 | x64) & (android | linux)) | (windows & !uwp)", + "dependencies": [ + { + "name": "vcpkg-cmake", + "host": true, + "platform": "windows & !mingw" + } + ] +} diff --git a/vcpkg.json b/vcpkg.json index 0b2b9ab4f9e..1ad85d18655 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -48,6 +48,16 @@ "name": "libyuv", "host": false }, + { + "name": "mfx-dispatch", + "host": true, + "platform": "((x86 | x64) & (android | linux)) | (windows & !uwp)" + }, + { + "name": "mfx-dispatch", + "host": false, + "platform": "((x86 | x64) & (android | linux)) | (windows & !uwp)" + }, { "name": "ffmpeg", "host": true, @@ -90,10 +100,6 @@ { "name": "amd-amf", "version": "1.4.35" - }, - { - "name": "mfx-dispatch", - "version": "1.35.1" } ] -} \ No newline at end of file +} From a7aacc78554c488876730bd1e7aeb6b991b19a9b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 5 Apr 2025 08:45:33 +0800 Subject: [PATCH 179/506] refact: win, dlopen mf (#11353) Signed-off-by: fufesou --- Cargo.lock | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eded3d37fd8..4c6d3e76c28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1789,7 +1789,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]] @@ -3643,7 +3643,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]] @@ -4189,7 +4189,7 @@ dependencies = [ [[package]] name = "nokhwa" version = "0.10.7" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#866453192f369eb40ab9051c67ae59f0fed809a4" dependencies = [ "flume", "image 0.25.1", @@ -4204,7 +4204,7 @@ dependencies = [ [[package]] name = "nokhwa-bindings-linux" version = "0.1.1" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#866453192f369eb40ab9051c67ae59f0fed809a4" dependencies = [ "nokhwa-core", "v4l", @@ -4213,7 +4213,7 @@ dependencies = [ [[package]] name = "nokhwa-bindings-macos" version = "0.2.2" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#866453192f369eb40ab9051c67ae59f0fed809a4" dependencies = [ "block", "cocoa-foundation", @@ -4229,8 +4229,10 @@ dependencies = [ [[package]] name = "nokhwa-bindings-windows" version = "0.4.2" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#866453192f369eb40ab9051c67ae59f0fed809a4" dependencies = [ + "dlopen", + "lazy_static", "nokhwa-core", "once_cell", "windows 0.43.0", @@ -4239,7 +4241,7 @@ dependencies = [ [[package]] name = "nokhwa-core" version = "0.1.5" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#3e2512074bc57d5df011363a26a8ee8959dc7969" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#866453192f369eb40ab9051c67ae59f0fed809a4" dependencies = [ "bytes", "image 0.25.1", From 62a83ad319ea7c605e068f13d415b1b3dd71825e Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 5 Apr 2025 10:00:34 +0800 Subject: [PATCH 180/506] fix: build (#11365) Signed-off-by: fufesou --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c6d3e76c28..8df6e6de089 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4189,7 +4189,7 @@ dependencies = [ [[package]] name = "nokhwa" version = "0.10.7" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#866453192f369eb40ab9051c67ae59f0fed809a4" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#48963f514bb7f1ee5db44d38dfc88027029de5e6" dependencies = [ "flume", "image 0.25.1", @@ -4204,7 +4204,7 @@ dependencies = [ [[package]] name = "nokhwa-bindings-linux" version = "0.1.1" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#866453192f369eb40ab9051c67ae59f0fed809a4" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#48963f514bb7f1ee5db44d38dfc88027029de5e6" dependencies = [ "nokhwa-core", "v4l", @@ -4213,7 +4213,7 @@ dependencies = [ [[package]] name = "nokhwa-bindings-macos" version = "0.2.2" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#866453192f369eb40ab9051c67ae59f0fed809a4" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#48963f514bb7f1ee5db44d38dfc88027029de5e6" dependencies = [ "block", "cocoa-foundation", @@ -4229,7 +4229,7 @@ dependencies = [ [[package]] name = "nokhwa-bindings-windows" version = "0.4.2" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#866453192f369eb40ab9051c67ae59f0fed809a4" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#48963f514bb7f1ee5db44d38dfc88027029de5e6" dependencies = [ "dlopen", "lazy_static", @@ -4241,7 +4241,7 @@ dependencies = [ [[package]] name = "nokhwa-core" version = "0.1.5" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#866453192f369eb40ab9051c67ae59f0fed809a4" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#48963f514bb7f1ee5db44d38dfc88027029de5e6" dependencies = [ "bytes", "image 0.25.1", From 2d403913b5802ec4ce3ceb5d675bb28b7b571bf9 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 6 Apr 2025 10:42:15 +0800 Subject: [PATCH 181/506] fix: enigo, macos, F11 (#11371) Signed-off-by: fufesou --- libs/enigo/src/macos/macos_impl.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index e7d7d9e8d33..92e202ef967 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -142,7 +142,8 @@ impl Enigo { } fn post(&self, event: CGEvent) { - if !self.ignore_flags { + // event.set_flags(CGEventFlags::CGEventFlagNull); will cause `F11` not working. no idea why. + if !self.ignore_flags && self.flags != CGEventFlags::CGEventFlagNull { event.set_flags(self.flags); } event.set_integer_value_field(EventField::EVENT_SOURCE_USER_DATA, ENIGO_INPUT_EXTRA_VALUE); From d972c0eda1c64482ad738c03b79de3f9d2fb5db6 Mon Sep 17 00:00:00 2001 From: asereze Date: Mon, 7 Apr 2025 08:04:03 +0200 Subject: [PATCH 182/506] Update sc.rs (#11342) Some fixes --- src/lang/sc.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/sc.rs b/src/lang/sc.rs index a8acc190964..71e31e29852 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -9,9 +9,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Established", "Istabilida"), ("connecting_status", "Connessione a sa rete RustDesk..."), ("Enable service", "Abìlita servìtziu"), - ("Start service", "Avia su servìtziu"), - ("Service is running", "Su servìtziu est in esecutzione"), - ("Service is not running", "Su servìtziu no est in esecutzione"), + ("Start service", "Allughe su servìtziu"), + ("Service is running", "Su servìtziu est in funtzione"), + ("Service is not running", "Su servìtziu no est in funtzione"), ("not_ready_status", "Non prontu. Verìfica sa connessione"), ("Control Remote Desktop", "Controlla s'elaboradore remotu"), ("Transfer file", "Tràmuda documentos"), @@ -146,9 +146,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set Password", "Imposta sa crae"), ("OS Password", "Crae sistema operativu"), ("install_tip", "Pro neghe de su Controllu Contu Utente (UAC), RustDesk diat pòdere non funtzionare comente si tocat comente iscrivania remota.\nPro evitare custu problema, incarca in su butone inoghe in suta pro installare RustDesk a livellu de sistema."), - ("Click to upgrade", "Incarca pro atualizare"), + ("Click to upgrade", "Atualiza"), ("Click to download", "Iscàrriga"), - ("Click to update", "Incarca pro annoare"), + ("Click to update", "Annoa"), ("Configure", "Cunfigura"), ("config_acc", "Pro controllare s'iscrivania dae foras, depes frunire a RustDesk su permissu 'Atzessibilidade'."), ("config_screen", "Pro controllare s'iscrivania dae foras, depes frunire a RustDesk su permissu 'Registratzione ischermu'."), @@ -677,7 +677,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("use-the-selected-printer-tip", "Imprea s'imprentadora seletzionada"), ("auto-print-tip", "Imprenta in automàticu impreende s'imprentadora seletzionada."), ("print-incoming-job-confirm-tip", "As retzidu unu traballu de imprenta dae remotu. Lu boles esecutare dae s'ala tua?"), - ("remote-printing-disallowed-tile-tip", "Impreanta remota disabilitada"), + ("remote-printing-disallowed-tile-tip", "Imprenta remota disabilitada"), ("remote-printing-disallowed-text-tip", "Sas impostatziones de sos permissos de s'ala controllada negant s'imprenta remota."), ("save-settings-tip", "Sarva sas impostatziones"), ("dont-show-again-tip", "Non mustres prus custu messàgiu"), From cc0761446f1c25cf20f16ee79f04420c9bb518de Mon Sep 17 00:00:00 2001 From: Marcos Rodrigo Ladeia Date: Mon, 7 Apr 2025 03:05:14 -0300 Subject: [PATCH 183/506] Update ptbr.rs (#11354) Improvements in translation for better user understanding. --- src/lang/ptbr.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 4cb7ca7d8dd..dc8faa18ac0 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -345,7 +345,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Light Theme", "Tema Claro"), ("Dark", "Escuro"), ("Light", "Claro"), - ("Follow System", "Seguir sistema"), + ("Follow System", "Padrão do sistema"), ("Enable hardware codec", "Habilitar codec de hardware"), ("Unlock Security Settings", "Desbloquear configurações de segurança"), ("Enable audio", "Habilitar áudio"), @@ -670,13 +670,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-not-installed-tip", "A impressora {} não está instalada."), ("printer-{}-ready-tip", "A impressora {} está instalada e operacional."), ("Install {} Printer", "Instalar impressora {}"), - ("Outgoing Print Jobs", "Impressões de saída"), - ("Incomming Print Jobs", "Impressões recebidas"), - ("Incoming Print Job", "Trabalho de impressão recebido"), + ("Outgoing Print Jobs", "Trabalhos de impressão enviados"), + ("Incomming Print Jobs", "Trabalhos de impressão recebidos"), + ("Incoming Print Job", "Impressão recebida"), ("use-the-default-printer-tip", "Usar impressora padrão"), ("use-the-selected-printer-tip", "Usar impressora selecionada"), ("auto-print-tip", "Imprimir automaticamente usando a impressora selecionada."), - ("print-incoming-job-confirm-tip", "O dispositivo remoto enviou uma impressão; deseja executá-la no seu lado?"), + ("print-incoming-job-confirm-tip", "O dispositivo remoto enviou uma impressão. Deseja imprimir?"), ("remote-printing-disallowed-tile-tip", "Impressão remota não permitida"), ("remote-printing-disallowed-text-tip", "As configurações do dispositivo controlado não permitem impressão remota."), ("save-settings-tip", "Salvar configurações"), From d8eb23a571f3887fd271111a1544a787e6e25517 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 7 Apr 2025 15:40:17 +0800 Subject: [PATCH 184/506] fix: msi, silent install, launch app tray (#11389) Signed-off-by: fufesou --- res/msi/Package/Components/RustDesk.wxs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index 4093a0189f2..ffe4f0ffbd6 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -56,7 +56,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 52b6541dd033c88c34edb266db8547978781a85e Mon Sep 17 00:00:00 2001 From: Yurt Page Date: Tue, 22 Apr 2025 03:17:43 +0300 Subject: [PATCH 207/506] docs: CONTRIBUTING-DE.md convert to UTF8 (#11523) --- docs/CONTRIBUTING-DE.md | 48 ++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/CONTRIBUTING-DE.md b/docs/CONTRIBUTING-DE.md index 6258a9a7a11..b45c23d502e 100644 --- a/docs/CONTRIBUTING-DE.md +++ b/docs/CONTRIBUTING-DE.md @@ -1,42 +1,42 @@ -# Beitrge zu RustDesk +# Beiträge zu RustDesk -RustDesk begrt Beitrge von jedem. Hier sind die Richtlinien, wenn Sie uns -helfen mchten: +RustDesk begrüßt Beiträge von jedem. Hier sind die Richtlinien, wenn Sie uns +helfen möchten: -## Beitrge +## Beiträge -Beitrge zu RustDesk oder seinen Abhngigkeiten sollten in Form von Pull +Beiträge zu RustDesk oder seinen Abhängigkeiten sollten in Form von Pull Requests auf GitHub erfolgen. Jeder Pull Request wird von einem Hauptakteur -(jemand mit der Erlaubnis, Korrekturen einzubringen) geprft und entweder in den -Hauptbaum eingefgt oder Feedback fr notwendige nderungen gegeben. Alle -Beitrge sollten diesem Format folgen, auch die von Hauptakteuren. +(jemand mit der Erlaubnis, Korrekturen einzubringen) geprüft und entweder in den +Hauptbaum eingefügt oder Feedback für notwendige Änderungen gegeben. Alle +Beiträge sollten diesem Format folgen, auch die von Hauptakteuren. -Wenn Sie an einem Problem arbeiten mchten, melden Sie es bitte zuerst an, indem -Sie auf GitHub erklren, dass Sie daran arbeiten mchten. Damit soll verhindert -werden, dass Beitrge zum gleichen Thema doppelt bearbeitet werden. +Wenn Sie an einem Problem arbeiten möchten, melden Sie es bitte zuerst an, indem +Sie auf GitHub erklären, dass Sie daran arbeiten möchten. Damit soll verhindert +werden, dass Beiträge zum gleichen Thema doppelt bearbeitet werden. -## Checkliste fr Pull Requests +## Checkliste für Pull Requests -- Verzweigen Sie sich vom Master-Branch und, falls ntig, wechseln Sie zum +- Verzweigen Sie sich vom Master-Branch und, falls nötig, wechseln Sie zum aktuellen Master-Branch, bevor Sie Ihren Pull Request einreichen. Wenn das - Zusammenfhren mit dem Master nicht reibungslos funktioniert, werden Sie - mglicherweise aufgefordert, Ihre nderungen zu berarbeiten. + Zusammenführen mit dem Master nicht reibungslos funktioniert, werden Sie + möglicherweise aufgefordert, Ihre Änderungen zu überarbeiten. -- Commits sollten so klein wie mglich sein und gleichzeitig sicherstellen, dass - jeder Commit unabhngig voneinander korrekt ist (d. h., jeder Commit sollte - sich bersetzen lassen und Tests bestehen). +- Commits sollten so klein wie möglich sein und gleichzeitig sicherstellen, dass + jeder Commit unabhängig voneinander korrekt ist (d. h., jeder Commit sollte + sich übersetzen lassen und Tests bestehen). -- Commits sollten von einem "Herkunftszertifikat fr Entwickler" +- Commits sollten von einem "Herkunftszertifikat für Entwickler" (https://developercertificate.org) begleitet werden, das besagt, dass Sie (und ggf. Ihr Arbeitgeber) mit den Bedingungen der [Projektlizenz](../LICENCE) - einverstanden sind. In Git ist dies die Option `-s` fr `git commit`. + einverstanden sind. In Git ist dies die Option `-s` für `git commit`. - Wenn Ihr Patch nicht begutachtet wird oder Sie eine bestimmte Person zur - Begutachtung bentigen, knnen Sie einem Gutachter mit @ antworten und um eine - Begutachtung des Pull Requests oder einen Kommentar bitten. Sie knnen auch + Begutachtung benötigen, können Sie einem Gutachter mit @ antworten und um eine + Begutachtung des Pull Requests oder einen Kommentar bitten. Sie können auch per [E-Mail](mailto:info@rustdesk.com) um eine Begutachtung bitten. -- Fgen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue +- Fügen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue Funktion beziehen. Spezifische Git-Anweisungen finden Sie im [GitHub-Workflow](https://github.com/servo/servo/wiki/GitHub-workflow). @@ -47,4 +47,4 @@ https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md ## Kommunikation -RustDesk-Mitarbeiter arbeiten hufig im [Discord](https://discord.gg/nDceKgxnkV). +RustDesk-Mitarbeiter arbeiten häufig im [Discord](https://discord.gg/nDceKgxnkV). From 57ee0318277c92ff040d92067dd0b5e6749bbdc9 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 22 Apr 2025 16:53:38 +0800 Subject: [PATCH 208/506] Revert "Translations update from Toolate (#11510)" (#11535) This reverts commit 86d9e6278024f32cd85f24420e33a1163cfb1fab. --- .../android/ru-RU/full_description.txt | 11 ---- .../android/ru-RU/short_description.txt | 1 - res/msi/Package/Language/Package.ru_ru.wxl | 55 ------------------- 3 files changed, 67 deletions(-) delete mode 100644 fastlane/metadata/android/ru-RU/full_description.txt delete mode 100644 fastlane/metadata/android/ru-RU/short_description.txt delete mode 100644 res/msi/Package/Language/Package.ru_ru.wxl diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt deleted file mode 100644 index c31380f30fc..00000000000 --- a/fastlane/metadata/android/ru-RU/full_description.txt +++ /dev/null @@ -1,11 +0,0 @@ -Свободное приложение удалённого подключения к рабочему столу, альтернатива TeamViewer но с открытым исходным кодом. -Исходный код: https://github.com/rustdesk/rustdesk -Документация: https://rustdesk.com/docs/en/manual/mobile/ - -Чтобы удалённо управлять вашим устройством Android с помощью мыши или прикосновения, вам необходимо позволить RustDesk использовать службу «доступности», RustDesk использует API AccessibilyService для реализации удалённого управления Андроидом. - -Кроме дистанционного управления, вы также можете легко переносить файлы между устройствами Андроид и ПК через RustDesk. - -Вы имеете полный контроль над своими данными, без проблем с безопасностью. Вы можете использовать наш релейный сервер, или самостоятельно поднять свой собственный релейный сервер. Релейный сервер бесплатный и с открытым исходным кодом: https://github.com/rustdesk/rustdesk-server - -Вы также можете загрузить и установить версию для настольных компьютеров с https://rustdesk.com, тогда вы сможете получить доступ и управлять своим рабочим столом с мобильного телефона или управлять мобильным устройством с рабочего стола. diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt deleted file mode 100644 index ef649b4bdcc..00000000000 --- a/fastlane/metadata/android/ru-RU/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Свободное приложение удалённого доступа к рабочему столу, как TeamViewer diff --git a/res/msi/Package/Language/Package.ru_ru.wxl b/res/msi/Package/Language/Package.ru_ru.wxl deleted file mode 100644 index 017f3824105..00000000000 --- a/res/msi/Package/Language/Package.ru_ru.wxl +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 2cb096178af35e5fb7dff5ebeccc7037db22a754 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 22 Apr 2025 22:05:46 +0800 Subject: [PATCH 209/506] ifix PRO --- src/hbbs_http/sync.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs index 5a073402736..21689e2e806 100644 --- a/src/hbbs_http/sync.rs +++ b/src/hbbs_http/sync.rs @@ -119,9 +119,11 @@ async fn start_hbbs_sync_async() { Ok(x) => { sysinfo_ver = x.clone(); x == ver + *PRO.lock().unwrap() = true; } _ => { - true // if failed to get sysinfo_ver, we assume it's the same version + false // to make sure Pro can be assigned in below post for old + // hbbs pro not supporting sysinfo_ver } }; if samever { From 296aa7f8a05e689b46d96b5a9ec8fb4cbd379ef6 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 22 Apr 2025 23:18:36 +0800 Subject: [PATCH 210/506] fix build and update comment (#11542) Signed-off-by: 21pages --- src/hbbs_http/sync.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs index 21689e2e806..3502f604f60 100644 --- a/src/hbbs_http/sync.rs +++ b/src/hbbs_http/sync.rs @@ -115,15 +115,16 @@ async fn start_hbbs_sync_async() { let old_hash = config::Status::get("sysinfo_hash"); let ver = config::Status::get("sysinfo_ver"); // sysinfo_ver is the version of sysinfo on server's side if hash == old_hash { + // When the api doesn't exist, Ok("") will be returned in test. let samever = match crate::post_request(url.replace("heartbeat", "sysinfo_ver"), "".to_owned(), "").await { Ok(x) => { sysinfo_ver = x.clone(); - x == ver *PRO.lock().unwrap() = true; + x == ver } _ => { false // to make sure Pro can be assigned in below post for old - // hbbs pro not supporting sysinfo_ver + // hbbs pro not supporting sysinfo_ver, use false for ensuring } }; if samever { From 5c2538e7af2c2fba3a0cd6acffc75ef52c5bd734 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 23 Apr 2025 22:24:43 +0800 Subject: [PATCH 211/506] https://github.com/rustdesk/rustdesk/discussions/9802 --- src/core_main.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 90c1261bb17..1c4d66c615e 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -277,11 +277,7 @@ pub fn core_main() -> Option> { .arg(&format!("{} --tray", crate::get_app_name().to_lowercase())) .status() .ok(); - hbb_common::allow_err!(crate::platform::run_as_user( - vec!["--tray"], - None, - None::<(&str, &str)>, - )); + hbb_common::allow_err!(crate::run_me(vec!["--tray"])); } #[cfg(windows)] crate::privacy_mode::restore_reg_connectivity(true); From 279fb72a4fa970192cbe0d514a7c1228453b6709 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:24:22 +0800 Subject: [PATCH 212/506] fix: remote printer, update install option (#11461) * fix: remote printer, update install option Signed-off-by: fufesou * Add comments Signed-off-by: fufesou * Add comments Signed-off-by: fufesou * Win, run_cmds, remove extra whitespace and newline Signed-off-by: fufesou --------- Signed-off-by: fufesou --- .../desktop/pages/desktop_setting_page.dart | 6 ++-- src/flutter_ffi.rs | 10 ++++++ src/ipc.rs | 28 +++++++++++++++++ src/platform/windows.rs | 31 ++++++++++++++++++- src/server.rs | 2 ++ 5 files changed, 73 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index d21e7d347c5..9870437e45b 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1979,7 +1979,7 @@ class __PrinterState extends State<_Printer> { final installed = bind.mainIsInstalled(); // `is-printer-installed` may fail, but it's rare case. // Add additional error message here if it's really needed. - final driver_installed = + final isPrinterInstalled = bind.mainGetCommonSync(key: 'is-printer-installed') == 'true'; final List children = []; @@ -1988,8 +1988,8 @@ class __PrinterState extends State<_Printer> { } else { children.addAll([ if (!installed) tipClientNotInstalled(), - if (installed && !driver_installed) tipPrinterNotInstalled(), - if (installed && driver_installed) tipReady() + if (installed && !isPrinterInstalled) tipPrinterNotInstalled(), + if (installed && isPrinterInstalled) tipReady() ]); } return _Card(title: 'Outgoing Print Jobs', children: children); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 84b39dc8543..c446e6a83ed 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -2438,6 +2438,16 @@ pub fn main_set_common(_key: String, _value: String) { (false, err) } }; + if success { + // Use `ipc` to notify the server process to update the install option in the registry. + // Because `install_update_printer()` may prompt for permissions, there is no need to prompt again here. + if let Err(e) = crate::ipc::set_install_option( + crate::platform::REG_NAME_INSTALL_PRINTER.to_string(), + "1".to_string(), + ) { + log::error!("Failed to set install printer option: {}", e); + } + } let data = HashMap::from([ ("name", serde_json::json!("install-printer-res")), ("success", serde_json::json!(success)), diff --git a/src/ipc.rs b/src/ipc.rs index 07b0117401d..4ddceca27a7 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -274,6 +274,7 @@ pub enum Data { ClearTrustedDevices, #[cfg(all(target_os = "windows", feature = "flutter"))] PrinterData(Vec), + InstallOption(Option<(String, String)>), } #[tokio::main(flavor = "current_thread")] @@ -662,6 +663,23 @@ async fn handle(data: Data, stream: &mut Connection) { Data::ClearTrustedDevices => { Config::clear_trusted_devices(); } + Data::InstallOption(opt) => match opt { + Some((_k, _v)) => { + #[cfg(target_os = "windows")] + if let Err(e) = crate::platform::windows::update_install_option(&_k, &_v) { + log::error!( + "Failed to update install option \"{}\" to \"{}\", error: {}", + &_k, + &_v, + e + ); + } + } + None => { + // `None` is usually used to get values. + // This branch is left blank for unification and further use. + } + }, _ => {} } } @@ -1277,6 +1295,16 @@ async fn handle_wayland_screencast_restore_token( return Ok(None); } +#[tokio::main(flavor = "current_thread")] +pub async fn set_install_option(k: String, v: String) -> ResultType<()> { + if let Ok(mut c) = connect(1000, "").await { + c.send(&&Data::InstallOption(Some((k, v)))).await?; + // do not put below before connect, because we need to check should_exit + c.next_timeout(1000).await.ok(); + } + Ok(()) +} + #[cfg(test)] mod test { use super::*; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 2c89b49fbaa..015b6a3328b 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -76,7 +76,7 @@ pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW"; const REG_NAME_INSTALL_DESKTOPSHORTCUTS: &str = "DESKTOPSHORTCUTS"; const REG_NAME_INSTALL_STARTMENUSHORTCUTS: &str = "STARTMENUSHORTCUTS"; -const REG_NAME_INSTALL_PRINTER: &str = "PRINTER"; +pub const REG_NAME_INSTALL_PRINTER: &str = "PRINTER"; pub fn get_focused_display(displays: Vec) -> Option { unsafe { @@ -1590,6 +1590,35 @@ pub fn get_license_from_exe_name() -> ResultType { get_custom_server_from_string(&exe) } +pub fn check_update_printer_option() { + if !is_installed() { + return; + } + let app_name = crate::get_app_name(); + if let Ok(b) = remote_printer::is_rd_printer_installed(&app_name) { + let v = if b { "1" } else { "0" }; + if let Err(e) = update_install_option(REG_NAME_INSTALL_PRINTER, v) { + log::error!( + "Failed to update printer option \"{}\" to \"{}\", error: {}", + REG_NAME_INSTALL_PRINTER, + v, + e + ); + } + } +} + +// We can't directly use `RegKey::set_value` to update the registry value, because it will fail with `ERROR_ACCESS_DENIED` +// So we have to use `run_cmds` to update the registry value. +pub fn update_install_option(k: &str, v: &str) -> ResultType<()> { + let app_name = crate::get_app_name(); + let ext = app_name.to_lowercase(); + let cmds = + format!("chcp 65001 && reg add HKEY_CLASSES_ROOT\\.{ext} /f /v {k} /t REG_SZ /d \"{v}\""); + run_cmds(cmds, false, "update_install_option")?; + Ok(()) +} + #[inline] pub fn is_win_server() -> bool { unsafe { is_windows_server() > 0 } diff --git a/src/server.rs b/src/server.rs index 87e6f390f5f..7f51e5a9933 100644 --- a/src/server.rs +++ b/src/server.rs @@ -567,6 +567,8 @@ pub async fn start_server(is_server: bool, no_server: bool) { crate::platform::try_kill_broker(); #[cfg(feature = "hwcodec")] scrap::hwcodec::start_check_process(); + #[cfg(target_os = "windows")] + crate::platform::check_update_printer_option(); crate::RendezvousMediator::start_all().await; } else { match crate::ipc::connect(1000, "").await { From 198967ea357339b683fb5e71d3de302347193934 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 25 Apr 2025 10:37:09 +0800 Subject: [PATCH 213/506] fix: allow logon screen password, on lock screen (#11566) Signed-off-by: fufesou --- Cargo.lock | 106 +++++++++++++++++++++++++++++++++++++-- Cargo.toml | 7 +++ libs/hbb_common | 2 +- src/platform/linux.rs | 25 +++++++++ src/platform/macos.rs | 32 ++++++++++++ src/platform/windows.rs | 60 +++++++++++++++++++++- src/server/connection.rs | 25 ++++++++- 7 files changed, 248 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8ccaf5fc62..d28cfc15a6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5666,7 +5666,7 @@ version = "0.1.0" dependencies = [ "hbb_common", "winapi 0.3.9", - "windows-strings", + "windows-strings 0.3.1", ] [[package]] @@ -5947,6 +5947,7 @@ dependencies = [ "wallpaper", "whoami", "winapi 0.3.9", + "windows 0.61.1", "windows-service", "winreg 0.11.0", "winres", @@ -6734,7 +6735,7 @@ dependencies = [ "unicode-segmentation", "url", "windows 0.52.0", - "windows-implement", + "windows-implement 0.52.0", "windows-version", "x11-dl", "zbus", @@ -7923,8 +7924,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core 0.52.0", - "windows-implement", - "windows-interface", + "windows-implement 0.52.0", + "windows-interface 0.52.0", "windows-targets 0.52.5", ] @@ -7938,6 +7939,28 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "windows" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +dependencies = [ + "windows-collections", + "windows-core 0.61.0", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.0", +] + [[package]] name = "windows-core" version = "0.51.1" @@ -7962,10 +7985,33 @@ version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.5", ] +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.2", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-future" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" +dependencies = [ + "windows-core 0.61.0", + "windows-link", +] + [[package]] name = "windows-implement" version = "0.52.0" @@ -7977,6 +8023,17 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + [[package]] name = "windows-interface" version = "0.52.0" @@ -7988,12 +8045,33 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + [[package]] name = "windows-link" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.0", + "windows-link", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -8003,6 +8081,15 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-service" version = "0.6.0" @@ -8023,6 +8110,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 078854bcdbf..2fb22dc296d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,6 +118,13 @@ winapi = { version = "0.3", features = [ "ioapiset", "winspool", ] } +windows = { version = "0.61.1", features = [ + "Win32", + "Win32_System", + "Win32_System_Diagnostics", + "Win32_System_Threading", + "Win32_System_Diagnostics_ToolHelp", +] } winreg = "0.11" windows-service = "0.6" virtual_display = { path = "libs/virtual_display" } diff --git a/libs/hbb_common b/libs/hbb_common index 1ed5a469cfa..ebb4d4a48cf 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 1ed5a469cfa2318b4045859cab6c88889717193a +Subproject commit ebb4d4a48cf7ed6ca62e93f8ed124065c6408536 diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 08cf0fb9a90..5c1a1cf2c3b 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -623,6 +623,31 @@ pub fn is_prelogin() -> bool { n < 4 && n > 1 } +// Check "Lock". +// "Switch user" can't be checked, because `get_values_of_seat0(&[0])` does not return the session. +// The logged in session is "online" not "active". +// And the "Switch user" screen is usually Wayland login session, which we do not support. +pub fn is_locked() -> bool { + if is_prelogin() { + return false; + } + + let values = get_values_of_seat0(&[0]); + // Though the values can't be empty, we still add check here for safety. + // Because we cannot guarantee whether the internal implementation will change in the future. + // https://github.com/rustdesk/hbb_common/blob/ebb4d4a48cf7ed6ca62e93f8ed124065c6408536/src/platform/linux.rs#L119 + if values.is_empty() { + log::debug!("Failed to check is locked, values vector is empty."); + return false; + } + let session = &values[0]; + if session.is_empty() { + log::debug!("Failed to check is locked, session is empty."); + return false; + } + is_session_locked(session) +} + pub fn is_root() -> bool { crate::username() == "root" } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 2fb2b46db90..853e7edfda8 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -491,6 +491,38 @@ pub fn is_prelogin() -> bool { get_active_userid() == "0" } +// https://stackoverflow.com/questions/11505255/osx-check-if-the-screen-is-locked +// No "CGSSessionScreenIsLocked" can be found when macOS is not locked. +// +// `ioreg -n Root -d1` returns `"CGSSessionScreenIsLocked"=Yes` +// `ioreg -n Root -d1 -a` returns +// ``` +// ... +// CGSSessionScreenIsLocked +// +// ... +// ``` +pub fn is_locked() -> bool { + match std::process::Command::new("ioreg") + .arg("-n") + .arg("Root") + .arg("-d1") + .output() + { + Ok(output) => { + let output_str = String::from_utf8_lossy(&output.stdout); + // Although `"CGSSessionScreenIsLocked"=Yes` was printed on my macOS, + // I also check `"CGSSessionScreenIsLocked"=true` for better compability. + output_str.contains("\"CGSSessionScreenIsLocked\"=Yes") + || output_str.contains("\"CGSSessionScreenIsLocked\"=true") + } + Err(e) => { + log::error!("Failed to query ioreg for the lock state: {}", e); + false + } + } +} + pub fn is_root() -> bool { crate::username() == "root" } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 015b6a3328b..087ca485df1 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -21,7 +21,10 @@ use std::{ fs, io::{self, prelude::*}, mem, - os::{raw::c_ulong, windows::process::CommandExt}, + os::{ + raw::c_ulong, + windows::{ffi::OsStringExt, process::CommandExt}, + }, path::*, ptr::null_mut, sync::{atomic::Ordering, Arc, Mutex}, @@ -60,6 +63,13 @@ use winapi::{ winuser::*, }, }; +use windows::Win32::{ + Foundation::{CloseHandle as WinCloseHandle, HANDLE as WinHANDLE}, + System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, + TH32CS_SNAPPROCESS, + }, +}; use windows_service::{ define_windows_service, service::{ @@ -956,6 +966,19 @@ pub fn is_prelogin() -> bool { username.is_empty() || username == "SYSTEM" } +// `is_logon_ui()` is regardless of multiple sessions now. +// It only check if "LogonUI.exe" exists. +// +// If there're mulitple sessions (logged in users), +// some are in the login screen, while the others are not. +// Then this function may not work fine if the session we want to handle(connect) is not in the login screen. +// But it's a rare case and cannot be simply handled, so it will not be dealt with for the time being. +#[inline] +pub fn is_logon_ui() -> ResultType { + let pids = get_pids("LogonUI.exe")?; + Ok(!pids.is_empty()) +} + pub fn is_root() -> bool { // https://stackoverflow.com/questions/4023586/correct-way-to-find-out-if-a-service-is-running-as-the-system-user unsafe { is_local_system() == TRUE } @@ -2916,3 +2939,38 @@ pub fn send_raw_data_to_printer(printer_name: Option, data: Vec) -> Ok(()) } + +fn get_pids>(name: S) -> ResultType> { + let name = name.as_ref().to_lowercase(); + let mut pids = Vec::new(); + + unsafe { + let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)?; + if snapshot == WinHANDLE::default() { + return Ok(pids); + } + + let mut entry: PROCESSENTRY32W = std::mem::zeroed(); + entry.dwSize = std::mem::size_of::() as u32; + + if Process32FirstW(snapshot, &mut entry).is_ok() { + loop { + let proc_name = OsString::from_wide(&entry.szExeFile) + .to_string_lossy() + .to_lowercase(); + + if proc_name.contains(&name) { + pids.push(entry.th32ProcessID); + } + + if !Process32NextW(snapshot, &mut entry).is_ok() { + break; + } + } + } + + let _ = WinCloseHandle(snapshot); + } + + Ok(pids) +} diff --git a/src/server/connection.rs b/src/server/connection.rs index 24038effda0..46e19315db6 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1946,6 +1946,27 @@ impl Connection { return true; } + // https://github.com/rustdesk/rustdesk-server-pro/discussions/646 + // `is_logon` is used to check login with `OPTION_ALLOW_LOGON_SCREEN_PASSWORD` == "Y". + // `is_logon_ui()` is used on Windows, because there's no good way to detect `is_locked()`. + // Detecting `is_logon_ui()` (if `LogonUI.exe` running) is a workaround. + #[cfg(target_os = "windows")] + let is_logon = || { + crate::platform::is_prelogin() || { + match crate::platform::is_logon_ui() { + Ok(result) => result, + Err(e) => { + log::error!("Failed to detect logon UI: {:?}", e); + false + } + } + } + }; + #[cfg(any(target_os = "linux", target_os = "macos"))] + let is_logon = || crate::platform::is_prelogin() || crate::platform::is_locked(); + #[cfg(any(target_os = "android", target_os = "ios"))] + let is_logon = || crate::platform::is_prelogin(); + if !hbb_common::is_ip_str(&lr.username) && !hbb_common::is_domain_port_str(&lr.username) && lr.username != Config::get_id() @@ -1954,8 +1975,8 @@ impl Connection { .await; return false; } else if (password::approve_mode() == ApproveMode::Click - && !(crate::platform::is_prelogin() - && crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y")) + && !(crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y" + && is_logon())) || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() { self.try_start_cm(lr.my_id, lr.my_name, false); From c0789a5fc0b6bf84907769c8489b3c1307aa52ee Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 25 Apr 2025 17:13:19 +0800 Subject: [PATCH 214/506] Add custom client judgment for hide cm (#11563) There is latency in the HTTP request; add a custom client check to avoid the PRO variable being unset during application startup Signed-off-by: 21pages --- src/ipc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ipc.rs b/src/ipc.rs index 4ddceca27a7..43c082ae5d0 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -494,7 +494,7 @@ async fn handle(data: Data, stream: &mut Connection) { None }; } else if name == "hide_cm" { - value = if crate::hbbs_http::sync::is_pro() { + value = if crate::hbbs_http::sync::is_pro() || crate::common::is_custom_client() { Some(hbb_common::password_security::hide_cm().to_string()) } else { None From f438bf582b8f7516bc46b8617944626baa1ad22a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 26 Apr 2025 13:04:41 +0800 Subject: [PATCH 215/506] fix: http proxy (#11570) Signed-off-by: fufesou --- src/hbbs_http/http_client.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/hbbs_http/http_client.rs b/src/hbbs_http/http_client.rs index 944e84ae6ff..8d6f529b75a 100644 --- a/src/hbbs_http/http_client.rs +++ b/src/hbbs_http/http_client.rs @@ -6,15 +6,17 @@ use reqwest::Client as AsyncClient; macro_rules! configure_http_client { ($builder:expr, $Client: ty) => {{ - let mut builder = $builder; + // https://github.com/rustdesk/rustdesk/issues/11569 + // https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.no_proxy + let mut builder = $builder.no_proxy(); let client = if let Some(conf) = Config::get_socks() { let proxy_result = Proxy::from_conf(&conf, None); match proxy_result { Ok(proxy) => { let proxy_setup = match &proxy.intercept { - ProxyScheme::Http { host, .. } =>{ reqwest::Proxy::http(format!("http://{}", host))}, - ProxyScheme::Https { host, .. } => {reqwest::Proxy::https(format!("https://{}", host))}, + ProxyScheme::Http { host, .. } =>{ reqwest::Proxy::all(format!("http://{}", host))}, + ProxyScheme::Https { host, .. } => {reqwest::Proxy::all(format!("https://{}", host))}, ProxyScheme::Socks5 { addr, .. } => { reqwest::Proxy::all(&format!("socks5://{}", addr)) } }; From 16e9e716b62c164e6c02dea2381497a154f2d96a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 27 Apr 2025 12:04:05 +0800 Subject: [PATCH 216/506] fix: check server running on Windows (#11578) Signed-off-by: fufesou --- src/core_main.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/core_main.rs b/src/core_main.rs index 1c4d66c615e..c58835da91d 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -77,7 +77,14 @@ pub fn core_main() -> Option> { } #[cfg(any(target_os = "linux", target_os = "windows"))] if args.is_empty() { - if crate::check_process("--server", false) && !crate::check_process("--tray", true) { + #[cfg(target_os = "linux")] + let is_server_running = crate::check_process("--server", false); + // We can use `crate::check_process("--server", false)` on Windows. + // Because `--server` process is the System user's process. We can't get the arguments in `check_process()`. + // We can assume that self service running means the server is also running on Windows. + #[cfg(target_os = "windows")] + let is_server_running = crate::platform::is_self_service_running(); + if is_server_running && !crate::check_process("--tray", true) { #[cfg(target_os = "linux")] hbb_common::allow_err!(crate::platform::check_autostart_config()); hbb_common::allow_err!(crate::run_me(vec!["--tray"])); From c9d5e15ac0c807da4e4f7c1d9312f4d245238a78 Mon Sep 17 00:00:00 2001 From: YinMo19 <144041694+YinMo19@users.noreply.github.com> Date: Mon, 28 Apr 2025 00:47:33 +0800 Subject: [PATCH 217/506] Using new Stream type adapted to the update of submodules (#11581) * [fix bug] fix all err stream type. * [update] update hbb_common. * [bug fix] Stream in other platform. --- Cargo.lock | 1 + libs/hbb_common | 2 +- src/client.rs | 6 ++---- src/client/io_loop.rs | 2 +- src/common.rs | 8 ++++---- src/rendezvous_mediator.rs | 5 ++--- 6 files changed, 11 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d28cfc15a6d..1aa7e4040c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3108,6 +3108,7 @@ dependencies = [ "tokio-tungstenite", "tokio-util", "toml 0.7.8", + "tungstenite", "url", "uuid", "winapi 0.3.9", diff --git a/libs/hbb_common b/libs/hbb_common index ebb4d4a48cf..3afaf649447 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit ebb4d4a48cf7ed6ca62e93f8ed124065c6408536 +Subproject commit 3afaf6494475ef58dcaaae6b4e6d2303cc3d632b diff --git a/src/client.rs b/src/client.rs index def76a0e171..3f4b60e9e49 100644 --- a/src/client.rs +++ b/src/client.rs @@ -58,7 +58,6 @@ use hbb_common::{ sha2::{Digest, Sha256}, socket_client::{connect_tcp, connect_tcp_local, ipv4_to_ipv6}, sodiumoxide::{base64, crypto::sign}, - tcp::FramedStream, timeout, tokio::{ self, @@ -3557,8 +3556,7 @@ pub mod peer_online { rendezvous_proto::*, sleep, socket_client::connect_tcp, - tcp::FramedStream, - ResultType, + ResultType, Stream, }; pub async fn query_online_states, Vec)>(ids: Vec, f: F) { @@ -3581,7 +3579,7 @@ pub mod peer_online { } } - async fn create_online_stream() -> ResultType { + 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(); diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 8d858e5df97..69ab0fb040c 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -351,7 +351,7 @@ impl Remote { #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] async fn handle_local_clipboard_msg( &self, - peer: &mut crate::client::FramedStream, + peer: &mut Stream, msg: Option, ) { match msg { diff --git a/src/common.rs b/src/common.rs index 26d25f789d7..5f9bd71fef1 100644 --- a/src/common.rs +++ b/src/common.rs @@ -27,7 +27,7 @@ use hbb_common::{ self, time::{Duration, Instant, Interval}, }, - ResultType, + ResultType, Stream, }; use crate::{ @@ -832,7 +832,7 @@ pub fn is_modifier(evt: &KeyEvent) -> bool { pub fn check_software_update() { if is_custom_client() { return; - } + } let opt = config::LocalConfig::get_option(config::keys::OPTION_ENABLE_CHECK_UPDATE); if config::option2bool(config::keys::OPTION_ENABLE_CHECK_UPDATE, &opt) { std::thread::spawn(move || allow_err!(check_software_update_())); @@ -1196,7 +1196,7 @@ pub fn pk_to_fingerprint(pk: Vec) -> String { #[inline] pub async fn get_next_nonkeyexchange_msg( - conn: &mut FramedStream, + conn: &mut Stream, timeout: Option, ) -> Option { let timeout = timeout.unwrap_or(READ_TIMEOUT); @@ -1265,7 +1265,7 @@ pub fn check_process(arg: &str, mut same_uid: bool) -> bool { false } -pub async fn secure_tcp(conn: &mut FramedStream, key: &str) -> ResultType<()> { +pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> { let rs_pk = get_rs_pk(key); let Some(rs_pk) = rs_pk else { bail!("Handshake failed: invalid public key from rendezvous server"); diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 69fc886cac9..d8ec3c102d5 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -20,10 +20,9 @@ use hbb_common::{ rendezvous_proto::*, sleep, socket_client::{self, connect_tcp, is_ipv4}, - tcp::FramedStream, tokio::{self, select, sync::Mutex, time::interval}, udp::FramedSocket, - AddrMangle, IntoTargetAddr, ResultType, TargetAddr, + AddrMangle, IntoTargetAddr, ResultType, Stream, TargetAddr, }; use crate::{ @@ -706,7 +705,7 @@ async fn direct_server(server: ServerPtr) { enum Sink<'a> { Framed(&'a mut FramedSocket, &'a TargetAddr<'a>), - Stream(&'a mut FramedStream), + Stream(&'a mut Stream), } impl Sink<'_> { From abde556695a2a16d1a8f05b5034f3420b55aba0b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 28 Apr 2025 21:48:14 +0800 Subject: [PATCH 218/506] fix: lan discovery (#11592) Signed-off-by: fufesou --- flutter/lib/common/widgets/peers_view.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 842374a66b6..94f4af035a6 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -501,6 +501,7 @@ class DiscoveredPeersView extends BasePeersView { Widget build(BuildContext context) { final widget = super.build(context); bind.mainLoadLanPeers(); + bind.mainDiscover(); return widget; } } From f0c5580f57cd006ff48f7e001a75001168dcaec8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 29 Apr 2025 23:05:25 +0800 Subject: [PATCH 219/506] cap display name --- src/client.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/client.rs b/src/client.rs index 3f4b60e9e49..3c3850d0a00 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2317,6 +2317,17 @@ impl LoginConfigHandler { if display_name.is_empty() { display_name = crate::username(); } + let display_name = display_name + .chars() + .enumerate() + .map(|(i, c)| { + if i == 0 { + c.to_uppercase().to_string() + } else { + c.to_string() + } + }) + .collect::(); #[cfg(not(target_os = "android"))] let my_platform = whoami::platform().to_string(); #[cfg(target_os = "android")] From 2864e1984a79ebfe7801538b70381911dc18e84c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 29 Apr 2025 23:27:43 +0800 Subject: [PATCH 220/506] improve cap --- src/client.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/client.rs b/src/client.rs index 3c3850d0a00..0019bb9757e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2318,16 +2318,21 @@ impl LoginConfigHandler { display_name = crate::username(); } let display_name = display_name - .chars() - .enumerate() - .map(|(i, c)| { - if i == 0 { - c.to_uppercase().to_string() - } else { - c.to_string() - } + .split_whitespace() + .map(|word| { + word.chars() + .enumerate() + .map(|(i, c)| { + if i == 0 { + c.to_uppercase().to_string() + } else { + c.to_string() + } + }) + .collect::() }) - .collect::(); + .collect::>() + .join(" "); #[cfg(not(target_os = "android"))] let my_platform = whoami::platform().to_string(); #[cfg(target_os = "android")] From c626c2414d7a51b7e628555996d48983fa8d6607 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:23:35 +0800 Subject: [PATCH 221/506] feat: take screenshot (#11591) * feat: take screenshot Signed-off-by: fufesou * screenshot, vram temp switch capturer Signed-off-by: fufesou * fix: misspelling Signed-off-by: fufesou * screenshot, taking Signed-off-by: fufesou * screenshot, rgba stride Signed-off-by: fufesou * Bumps 1.4.0 Signed-off-by: fufesou --------- Signed-off-by: fufesou --- .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/lib/common/widgets/toolbar.dart | 38 ++++- flutter/lib/consts.dart | 3 +- .../desktop/pages/desktop_setting_page.dart | 8 +- flutter/lib/mobile/pages/remote_page.dart | 10 +- flutter/lib/mobile/pages/settings_page.dart | 4 +- .../lib/mobile/pages/view_camera_page.dart | 6 +- flutter/lib/models/model.dart | 78 +++++++++- flutter/lib/models/printer_model.dart | 4 +- flutter/pubspec.yaml | 2 +- libs/hbb_common | 2 +- libs/portable/Cargo.toml | 2 +- libs/scrap/src/common/convert.rs | 37 +++++ libs/scrap/src/common/mod.rs | 1 + res/PKGBUILD | 2 +- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- res/rpm.spec | 2 +- src/client.rs | 2 + src/client/io_loop.rs | 14 ++ src/client/screenshot.rs | 99 ++++++++++++ src/common.rs | 10 ++ src/flutter.rs | 46 +++++- src/flutter_ffi.rs | 31 ++++ src/lang/ar.rs | 8 +- src/lang/be.rs | 8 +- src/lang/bg.rs | 8 +- src/lang/ca.rs | 8 +- src/lang/cn.rs | 10 +- src/lang/cs.rs | 8 +- src/lang/da.rs | 8 +- src/lang/de.rs | 8 +- src/lang/el.rs | 8 +- src/lang/en.rs | 2 + src/lang/eo.rs | 8 +- src/lang/es.rs | 8 +- src/lang/et.rs | 8 +- src/lang/eu.rs | 8 +- src/lang/fa.rs | 8 +- src/lang/fr.rs | 8 +- src/lang/ge.rs | 15 +- src/lang/he.rs | 8 +- src/lang/hr.rs | 8 +- src/lang/hu.rs | 8 +- src/lang/id.rs | 8 +- src/lang/it.rs | 8 +- src/lang/ja.rs | 8 +- src/lang/ko.rs | 8 +- src/lang/kz.rs | 8 +- src/lang/lt.rs | 8 +- src/lang/lv.rs | 8 +- src/lang/nb.rs | 8 +- src/lang/nl.rs | 8 +- src/lang/pl.rs | 8 +- src/lang/pt_PT.rs | 8 +- src/lang/ptbr.rs | 8 +- src/lang/ro.rs | 8 +- src/lang/ru.rs | 8 +- src/lang/sc.rs | 8 +- src/lang/sk.rs | 8 +- src/lang/sl.rs | 8 +- src/lang/sq.rs | 8 +- src/lang/sr.rs | 8 +- src/lang/sv.rs | 8 +- src/lang/ta.rs | 8 +- src/lang/template.rs | 8 +- src/lang/th.rs | 8 +- src/lang/tr.rs | 8 +- src/lang/tw.rs | 8 +- src/lang/uk.rs | 8 +- src/lang/vn.rs | 8 +- src/server/connection.rs | 10 ++ src/server/video_service.rs | 146 +++++++++++++++++- src/ui/header.tis | 25 +++ src/ui/msgbox.tis | 45 ++++++ src/ui/remote.rs | 11 ++ src/ui_session_interface.rs | 9 ++ 82 files changed, 948 insertions(+), 96 deletions(-) create mode 100644 src/client/screenshot.rs diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 56f7030b8a8..27b6118b90e 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -38,7 +38,7 @@ env: # https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174 # 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`. VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" - VERSION: "1.3.9" + VERSION: "1.4.0" 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 3370e9921da..962df73f1cb 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -17,7 +17,7 @@ env: TAG_NAME: "nightly" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" - VERSION: "1.3.9" + VERSION: "1.4.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 1aa7e4040c8..1afa949f1af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5857,7 +5857,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.9" +version = "1.4.0" dependencies = [ "android-wakelock", "android_logger", @@ -5960,7 +5960,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.9" +version = "1.4.0" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index 2fb22dc296d..ff0e8a8701f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.9" +version = "1.4.0" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 8e0f2590679..36297f0e18e 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.9 + version: 1.4.0 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index d117d17178d..59bcca92bda 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.9 + version: 1.4.0 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 4011110ddcb..c861a09774c 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; @@ -15,7 +16,7 @@ bool isEditOsPassword = false; class TTextMenu { final Widget child; - final VoidCallback onPressed; + final VoidCallback? onPressed; Widget? trailingIcon; bool divider; TTextMenu( @@ -294,6 +295,41 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ), onPressed: () => ffi.recordingModel.toggle())); } + + // to-do: + // 1. Web desktop + // 2. Mobile, copy the image to the clipboard + if (isDesktop) { + final isScreenshotSupported = bind.sessionGetCommonSync( + sessionId: sessionId, key: 'is_screenshot_supported', param: ''); + if ('true' == isScreenshotSupported) { + v.add(TTextMenu( + child: Text(ffi.ffiModel.timerScreenshot != null + ? '${translate('Taking screenshot')} ...' + : translate('Take screenshot')), + onPressed: ffi.ffiModel.timerScreenshot != null + ? null + : () { + if (pi.currentDisplay == kAllDisplayValue) { + msgBox( + sessionId, + 'custom-nook-nocancel-hasclose-info', + 'Take screenshot', + 'screenshot-merged-screen-not-supported-tip', + '', + ffi.dialogManager); + } else { + bind.sessionTakeScreenshot( + sessionId: sessionId, display: pi.currentDisplay); + ffi.ffiModel.timerScreenshot = + Timer(Duration(seconds: 30), () { + ffi.ffiModel.timerScreenshot = null; + }); + } + }, + )); + } + } // fingerprint if (!(isDesktop || isWebDesktop)) { v.add(TTextMenu( diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 63023443e5b..a4d60b0a674 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -220,7 +220,8 @@ const double kDefaultQuality = 50; const double kMaxQuality = 100; const double kMaxMoreQuality = 2000; -const String kKeyPrinterIncommingJobAction = 'printer-incomming-job-action'; +// incomming (should be incoming) is kept, because change it will break the previous setting. +const String kKeyPrinterIncomingJobAction = 'printer-incomming-job-action'; const String kValuePrinterIncomingJobDismiss = 'dismiss'; const String kValuePrinterIncomingJobDefault = ''; const String kValuePrinterIncomingJobSelected = 'selected'; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 9870437e45b..5ac53775ce0 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1908,7 +1908,7 @@ class __PrinterState extends State<_Printer> { final scrollController = ScrollController(); return ListView(controller: scrollController, children: [ outgoing(context), - incomming(context), + incoming(context), ]).marginOnly(bottom: _kListViewBottomMargin); } @@ -1995,15 +1995,15 @@ class __PrinterState extends State<_Printer> { return _Card(title: 'Outgoing Print Jobs', children: children); } - Widget incomming(BuildContext context) { + Widget incoming(BuildContext context) { onRadioChanged(String value) async { await bind.mainSetLocalOption( - key: kKeyPrinterIncommingJobAction, value: value); + key: kKeyPrinterIncomingJobAction, value: value); setState(() {}); } PrinterOptions printerOptions = PrinterOptions.load(); - return _Card(title: 'Incomming Print Jobs', children: [ + return _Card(title: 'Incoming Print Jobs', children: [ _Radio(context, value: kValuePrinterIncomingJobDismiss, groupValue: printerOptions.action, diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 27ce2271380..93f49f585f6 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -695,9 +695,9 @@ class _RemotePageState extends State with WidgetsBindingObserver { ); if (index != null) { if (index < mobileActionMenus.length) { - mobileActionMenus[index].onPressed.call(); + mobileActionMenus[index].onPressed?.call(); } else if (index < mobileActionMenus.length + more.length) { - menus[index - mobileActionMenus.length].onPressed.call(); + menus[index - mobileActionMenus.length].onPressed?.call(); } } }(); @@ -770,7 +770,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { elevation: 8, ); if (index != null && index < menus.length) { - menus[index].onPressed.call(); + menus[index].onPressed?.call(); } }); } @@ -1267,7 +1267,7 @@ void showOptions( title: resolution.child, onTap: () { close(); - resolution.onPressed(); + resolution.onPressed?.call(); }, )); } @@ -1279,7 +1279,7 @@ void showOptions( title: virtualDisplayMenu.child, onTap: () { close(); - virtualDisplayMenu.onPressed(); + virtualDisplayMenu.onPressed?.call(); }, )); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 83cfa2fb23a..600f807e832 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -243,7 +243,7 @@ class _SettingsState extends State with WidgetsBindingObserver { Widget build(BuildContext context) { Provider.of(context); final outgoingOnly = bind.isOutgoingOnly(); - final incommingOnly = bind.isIncomingOnly(); + final incomingOnly = bind.isIncomingOnly(); final customClientSection = CustomSettingsSection( child: Column( children: [ @@ -728,7 +728,7 @@ class _SettingsState extends State with WidgetsBindingObserver { }); }, ), - if (!incommingOnly) + if (!incomingOnly) SettingsTile.switchTile( title: Text(translate('Automatically record outgoing sessions')), diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart index afd24dc7e03..ac70a2dab8e 100644 --- a/flutter/lib/mobile/pages/view_camera_page.dart +++ b/flutter/lib/mobile/pages/view_camera_page.dart @@ -478,9 +478,9 @@ class _ViewCameraPageState extends State ); if (index != null) { if (index < mobileActionMenus.length) { - mobileActionMenus[index].onPressed.call(); + mobileActionMenus[index].onPressed?.call(); } else if (index < mobileActionMenus.length + more.length) { - menus[index - mobileActionMenus.length].onPressed.call(); + menus[index - mobileActionMenus.length].onPressed?.call(); } } }(); @@ -553,7 +553,7 @@ class _ViewCameraPageState extends State elevation: 8, ); if (index != null && index < menus.length) { - menus[index].onPressed.call(); + menus[index].onPressed?.call(); } }); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8091846c64d..52782fc95a1 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -34,6 +34,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:uuid/uuid.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:file_picker/file_picker.dart'; import '../common.dart'; import '../utils/image.dart' as img; @@ -119,6 +120,8 @@ class FfiModel with ChangeNotifier { RxBool waitForFirstImage = true.obs; bool isRefreshing = false; + Timer? timerScreenshot; + Rect? get rect => _rect; bool get isOriginalResolutionSet => _pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolutionSet ?? false; @@ -216,6 +219,7 @@ class FfiModel with ChangeNotifier { _timer = null; clearPermissions(); waitForImageTimer?.cancel(); + timerScreenshot?.cancel(); } setConnectionType(String peerId, bool secure, bool direct) { @@ -414,12 +418,82 @@ class FfiModel with ChangeNotifier { } } else if (name == "printer_request") { _handlePrinterRequest(evt, sessionId, peerId); + } else if (name == 'screenshot') { + _handleScreenshot(evt, sessionId, peerId); } else { debugPrint('Event is not handled in the fixed branch: $name'); } }; } + _handleScreenshot( + Map evt, SessionID sessionId, String peerId) { + timerScreenshot?.cancel(); + timerScreenshot = null; + final msg = evt['msg'] ?? ''; + final msgBoxType = 'custom-nook-nocancel-hasclose'; + final msgBoxTitle = 'Take screenshot'; + final dialogManager = parent.target!.dialogManager; + if (msg.isNotEmpty) { + msgBox(sessionId, msgBoxType, msgBoxTitle, msg, '', dialogManager); + } else { + final msgBoxText = 'screenshot-action-tip'; + + close() { + dialogManager.dismissAll(); + } + + saveAs() { + close(); + Future.delayed(Duration.zero, () async { + final ts = DateTime.now().millisecondsSinceEpoch ~/ 1000; + String? outputFile = await FilePicker.platform.saveFile( + dialogTitle: '${translate('Save as')}...', + fileName: 'screenshot_$ts.png', + allowedExtensions: ['png'], + type: FileType.custom, + ); + if (outputFile == null) { + bind.sessionHandleScreenshot(sessionId: sessionId, action: '2'); + } else { + final res = await bind.sessionHandleScreenshot( + sessionId: sessionId, action: '0:$outputFile'); + if (res.isNotEmpty) { + msgBox(sessionId, 'custom-nook-nocancel-hasclose-error', + 'Take screenshot', res, '', dialogManager); + } + } + }); + } + + copyToClipboard() { + bind.sessionHandleScreenshot(sessionId: sessionId, action: '1'); + close(); + } + + cancel() { + bind.sessionHandleScreenshot(sessionId: sessionId, action: '2'); + close(); + } + + final List buttons = [ + dialogButton('${translate('Save as')}...', onPressed: saveAs), + dialogButton('Copy to clipboard', onPressed: copyToClipboard), + dialogButton('Cancel', onPressed: cancel), + ]; + dialogManager.dismissAll(); + dialogManager.show( + (setState, close, context) => CustomAlertDialog( + title: null, + content: SelectionArea( + child: msgboxContent(msgBoxType, msgBoxTitle, msgBoxText)), + actions: buttons, + ), + tag: '$msgBoxType-$msgBoxTitle-$msgBoxTitle', + ); + } + } + _handlePrinterRequest( Map evt, SessionID sessionId, String peerId) { final id = evt['id']; @@ -451,7 +525,7 @@ class FfiModel with ChangeNotifier { if (saveSettings.value || dontShowAgain.value) { bind.mainSetLocalOption(key: kKeyPrinterSelected, value: printerName); bind.mainSetLocalOption( - key: kKeyPrinterIncommingJobAction, + key: kKeyPrinterIncomingJobAction, value: defaultOrSelectedGroupValue.value); } if (dontShowAgain.value) { @@ -463,7 +537,7 @@ class FfiModel with ChangeNotifier { onCancel() { if (dontShowAgain.value) { bind.mainSetLocalOption( - key: kKeyPrinterIncommingJobAction, + key: kKeyPrinterIncomingJobAction, value: kValuePrinterIncomingJobDismiss); } close(); diff --git a/flutter/lib/models/printer_model.dart b/flutter/lib/models/printer_model.dart index df8f67cff37..8d0b3793257 100644 --- a/flutter/lib/models/printer_model.dart +++ b/flutter/lib/models/printer_model.dart @@ -13,7 +13,7 @@ class PrinterOptions { required this.printerName}); static PrinterOptions load() { - var action = bind.mainGetLocalOption(key: kKeyPrinterIncommingJobAction); + var action = bind.mainGetLocalOption(key: kKeyPrinterIncomingJobAction); if (![ kValuePrinterIncomingJobDismiss, kValuePrinterIncomingJobDefault, @@ -28,7 +28,7 @@ class PrinterOptions { if (action == kValuePrinterIncomingJobSelected) { action = kValuePrinterIncomingJobDefault; bind.mainSetLocalOption( - key: kKeyPrinterIncommingJobAction, + key: kKeyPrinterIncomingJobAction, value: kValuePrinterIncomingJobDefault); if (printerNames.isEmpty) { selectedPrinterName = ''; diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index d16365fa554..ac9b753fc15 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.9+57 +version: 1.4.0+58 environment: sdk: '^3.1.0' diff --git a/libs/hbb_common b/libs/hbb_common index 3afaf649447..42aad01a517 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 3afaf6494475ef58dcaaae6b4e6d2303cc3d632b +Subproject commit 42aad01a517d4de7420ddcb7e99c29bd9f6d2b5a diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index 418e122f734..2855b3cb6a7 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.9" +version = "1.4.0" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/libs/scrap/src/common/convert.rs b/libs/scrap/src/common/convert.rs index d3883192891..40c17e5bad2 100644 --- a/libs/scrap/src/common/convert.rs +++ b/libs/scrap/src/common/convert.rs @@ -197,3 +197,40 @@ pub fn convert_to_yuv( } Ok(()) } + +#[cfg(not(target_os = "ios"))] +pub fn convert(captured: &PixelBuffer, pixfmt: crate::Pixfmt, dst: &mut Vec) -> ResultType<()> { + if captured.pixfmt() == pixfmt { + dst.extend_from_slice(captured.data()); + return Ok(()); + } + + let src = captured.data(); + let src_stride = captured.stride(); + let src_pixfmt = captured.pixfmt(); + let src_width = captured.width(); + let src_height = captured.height(); + + let unsupported = format!( + "unsupported pixfmt conversion: {src_pixfmt:?} -> {:?}", + pixfmt + ); + + match (src_pixfmt, pixfmt) { + (crate::Pixfmt::BGRA, crate::Pixfmt::RGBA) | (crate::Pixfmt::RGBA, crate::Pixfmt::BGRA) => { + dst.resize(src.len(), 0); + call_yuv!(ABGRToARGB( + src.as_ptr(), + src_stride[0] as _, + dst.as_mut_ptr(), + src_stride[0] as _, + src_width as _, + src_height as _, + )); + } + _ => { + bail!(unsupported); + } + } + Ok(()) +} diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index af542197340..2d74caa0dd5 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -63,6 +63,7 @@ pub enum ImageFormat { } #[repr(C)] +#[derive(Clone)] pub struct ImageRgb { pub raw: Vec, pub w: usize, diff --git a/res/PKGBUILD b/res/PKGBUILD index b5ed113700a..a5601bf31c8 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.9 +pkgver=1.4.0 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 421bcb25a8c..1f566c6ecd4 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.9 +Version: 1.4.0 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 508b8050fcd..7323c92f263 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.9 +Version: 1.4.0 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index ebfefb8a080..0a64cbb3ca4 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.9 +Version: 1.4.0 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/src/client.rs b/src/client.rs index 0019bb9757e..dbad64675d7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -85,6 +85,7 @@ pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; +pub mod screenshot; pub const MILLI1: Duration = Duration::from_millis(1); pub const SEC30: Duration = Duration::from_secs(30); @@ -3336,6 +3337,7 @@ pub enum Data { CloseVoiceCall, ResetDecoder(Option), RenameFile((i32, String, String, bool)), + TakeScreenshot((i32, String)), } /// Keycode for key events. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 69ab0fb040c..687c942d9ed 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -962,6 +962,15 @@ impl Remote { } } }, + Data::TakeScreenshot((display, sid)) => { + let mut msg = Message::new(); + msg.set_screenshot_request(ScreenshotRequest { + display, + sid, + ..Default::default() + }); + allow_err!(peer.send(&msg).await); + } _ => {} } true @@ -1909,6 +1918,11 @@ impl Remote { self.handler.set_displays(&pi.displays); self.handler.set_platform_additions(&pi.platform_additions); } + Some(message::Union::ScreenshotResponse(response)) => { + crate::client::screenshot::set_screenshot(response.data); + self.handler + .handle_screenshot_resp(response.sid, response.msg); + } _ => {} } } diff --git a/src/client/screenshot.rs b/src/client/screenshot.rs new file mode 100644 index 00000000000..82a95bee96a --- /dev/null +++ b/src/client/screenshot.rs @@ -0,0 +1,99 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::{update_clipboard, ClipboardSide}; +use hbb_common::{message_proto::*, ResultType}; +use std::sync::Mutex; + +lazy_static::lazy_static! { + static ref SCREENSHOT: Mutex = Default::default(); +} + +pub enum ScreenshotAction { + SaveAs(String), + CopyToClipboard, + Discard, +} + +impl Default for ScreenshotAction { + fn default() -> Self { + Self::Discard + } +} + +impl From<&str> for ScreenshotAction { + fn from(value: &str) -> Self { + match value.chars().next() { + Some('0') => { + if let Some((pos, _)) = value.char_indices().nth(2) { + let substring = &value[pos..]; + Self::SaveAs(substring.to_string()) + } else { + Self::default() + } + } + Some('1') => Self::CopyToClipboard, + Some('2') => Self::default(), + _ => Self::default(), + } + } +} + +impl Into for ScreenshotAction { + fn into(self) -> String { + match self { + Self::SaveAs(p) => format!("0:{p}"), + Self::CopyToClipboard => "1".to_owned(), + Self::Discard => "2".to_owned(), + } + } +} + +#[derive(Default)] +pub struct Screenshot { + data: Option, +} + +impl Screenshot { + fn set_screenshot(&mut self, data: bytes::Bytes) { + self.data.replace(data); + } + + fn handle_screenshot(&mut self, action: String) -> String { + let Some(data) = self.data.take() else { + return "No cached screenshot".to_owned(); + }; + match Self::handle_screenshot_(data, action) { + Ok(()) => "".to_owned(), + Err(e) => e.to_string(), + } + } + + fn handle_screenshot_(data: bytes::Bytes, action: String) -> ResultType<()> { + match ScreenshotAction::from(&action as &str) { + ScreenshotAction::SaveAs(p) => { + std::fs::write(p, data)?; + } + ScreenshotAction::CopyToClipboard => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let clips = vec![Clipboard { + compress: false, + content: data, + format: ClipboardFormat::ImagePng.into(), + ..Default::default() + }]; + update_clipboard(clips, ClipboardSide::Client); + } + } + ScreenshotAction::Discard => {} + } + Ok(()) + } +} + +pub fn set_screenshot(data: bytes::Bytes) { + SCREENSHOT.lock().unwrap().set_screenshot(data); +} + +pub fn handle_screenshot(action: String) -> String { + SCREENSHOT.lock().unwrap().handle_screenshot(action) +} diff --git a/src/common.rs b/src/common.rs index 5f9bd71fef1..f8d5571b2f9 100644 --- a/src/common.rs +++ b/src/common.rs @@ -147,6 +147,16 @@ pub fn is_support_file_paste_if_macos(ver: &str) -> bool { hbb_common::get_version_number(ver) >= hbb_common::get_version_number("1.3.9") } +#[inline] +pub fn is_support_screenshot(ver: &str) -> bool { + is_support_multi_ui_session_num(hbb_common::get_version_number(ver)) +} + +#[inline] +pub fn is_support_screenshot_num(ver: i64) -> bool { + ver >= hbb_common::get_version_number("1.4.0") +} + // is server process, with "--server" args #[inline] pub fn is_server() -> bool { diff --git a/src/flutter.rs b/src/flutter.rs index 13c7bdd153d..ff9a787e3b5 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -543,6 +543,25 @@ impl FlutterHandler { pub fn push_event(&self, name: &str, event: &[(&str, V)], excludes: &[&SessionID]) where V: Sized + Serialize + Clone, + { + self.push_event_(name, event, &[], excludes); + } + + pub fn push_event_to(&self, name: &str, event: &[(&str, V)], include: &[&SessionID]) + where + V: Sized + Serialize + Clone, + { + self.push_event_(name, event, include, &[]); + } + + pub fn push_event_( + &self, + name: &str, + event: &[(&str, V)], + includes: &[&SessionID], + excludes: &[&SessionID], + ) where + V: Sized + Serialize + Clone, { let mut h: HashMap<&str, serde_json::Value> = event.iter().map(|(k, v)| (*k, json!(*v))).collect(); @@ -550,11 +569,20 @@ impl FlutterHandler { h.insert("name", json!(name)); let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); for (sid, session) in self.session_handlers.read().unwrap().iter() { - if excludes.contains(&sid) { - continue; + let mut push = false; + if includes.is_empty() { + if !excludes.contains(&sid) { + push = true; + } + } else { + if includes.contains(&sid) { + push = true; + } } - if let Some(stream) = &session.event_stream { - stream.add(EventToUI::Event(out.clone())); + if push { + if let Some(stream) = &session.event_stream { + stream.add(EventToUI::Event(out.clone())); + } } } } @@ -1067,6 +1095,16 @@ impl InvokeUiSession for FlutterHandler { &[], ); } + + fn handle_screenshot_resp(&self, sid: String, msg: String) { + match SessionID::from_str(&sid) { + Ok(sid) => self.push_event_to("screenshot", &[("msg", json!(msg))], &[&sid]), + Err(e) => { + // Unreachable! + log::error!("Failed to parse sid \"{}\", {}", sid, e); + } + } + } } impl FlutterHandler { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index c446e6a83ed..6e3932bcbad 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -250,6 +250,16 @@ pub fn session_refresh(session_id: SessionID, display: usize) { } } +pub fn session_take_screenshot(session_id: SessionID, display: usize) { + if let Some(s) = sessions::get_session_by_session_id(&session_id) { + s.take_screenshot(display as _, session_id.to_string()); + } +} + +pub fn session_handle_screenshot(session_id: SessionID, action: String) -> String { + crate::client::screenshot::handle_screenshot(action) +} + pub fn session_is_multi_ui_session(session_id: SessionID) -> SyncReturn { if let Some(session) = sessions::get_session_by_session_id(&session_id) { SyncReturn(session.is_multi_ui_session()) @@ -2461,6 +2471,27 @@ pub fn main_set_common(_key: String, _value: String) { } } +pub fn session_get_common_sync( + session_id: SessionID, + key: String, + param: String, +) -> SyncReturn> { + SyncReturn(session_get_common(session_id, key, param)) +} + +pub fn session_get_common(session_id: SessionID, key: String, param: String) -> Option { + if let Some(s) = sessions::get_session_by_session_id(&session_id) { + let v = if key == "is_screenshot_supported" { + s.is_screenshot_supported().to_string() + } else { + "".to_owned() + }; + Some(v) + } else { + None + } +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 1112bea777e..c3823b150a9 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "الطابعة {} جاهزة"), ("Install {} Printer", "تثبيت طابعة {}"), ("Outgoing Print Jobs", "وظائف الطباعة الصادرة"), - ("Incomming Print Jobs", "وظائف الطباعة الواردة"), + ("Incoming Print Jobs", "وظائف الطباعة الواردة"), ("Incoming Print Job", "وظيفة طباعة واردة"), ("use-the-default-printer-tip", "استخدم الطابعة الافتراضية"), ("use-the-selected-printer-tip", "استخدم الطابعة المحددة"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "الطباعة عن بُعد غير مسموح بها على هذا الجهاز"), ("save-settings-tip", "حفظ الإعدادات"), ("dont-show-again-tip", "لا تظهر هذا مرة أخرى"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index b618d21f82f..64e13ca8df5 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index e11e7db4a81..e14c5415daa 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 8120ace84b5..74c1d696efc 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 0297af774b7..3cbea8a1efe 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.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", "更新客户端的粘贴板"), + ("Update client clipboard", "更新客户端的剪贴板"), ("Untagged", "无标签"), ("new-version-of-{}-tip", "{} 版本更新"), ("Accessible devices", "可访问的设备"), @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "{} 打印机已安装,您可以使用打印功能了。"), ("Install {} Printer", "安装 {} 打印机"), ("Outgoing Print Jobs", "传出的打印任务"), - ("Incomming Print Jobs", "传入的打印任务"), + ("Incoming Print Jobs", "传入的打印任务"), ("Incoming Print Job", "传入的打印任务"), ("use-the-default-printer-tip", "使用默认的打印机执行"), ("use-the-selected-printer-tip", "使用选择的打印机执行"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "被控端的权限设置拒绝了远程打印。"), ("save-settings-tip", "保存设置"), ("dont-show-again-tip", "不再显示此信息"), + ("Take screenshot", "截屏"), + ("Taking screenshot", "正在截屏"), + ("screenshot-merged-screen-not-supported-tip", "当前不支持多个屏幕的合并截屏,请切换到单个屏幕重试。"), + ("screenshot-action-tip", "请选择如何继续截屏。"), + ("Save as", "另存为"), + ("Copy to clipboard", "复制到剪贴板"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 6f287a03c7c..f5f9acb68a2 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 32bd528810b..c8ea4532789 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index aaee6afecc1..221b786ddf8 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "Der Drucker {} ist installiert und einsatzbereit."), ("Install {} Printer", "Drucker {} installieren"), ("Outgoing Print Jobs", "Ausgehende Druckaufträge"), - ("Incomming Print Jobs", "Eingehende Druckaufträge"), + ("Incoming Print Jobs", "Eingehende Druckaufträge"), ("Incoming Print Job", "Eingehender Druckauftrag"), ("use-the-default-printer-tip", "Standarddrucker verwenden"), ("use-the-selected-printer-tip", "Ausgewählten Drucker verwenden"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "Die Berechtigungseinstellungen der kontrollierten Seite verweigern den entfernten Druck."), ("save-settings-tip", "Einstellungen speichern"), ("dont-show-again-tip", "Nicht mehr anzeigen"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index dc23cd03daa..0688020eddd 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 04d661ec923..ec7c745b1fd 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -253,5 +253,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "The permission settings of the controlled side deny Remote Printing."), ("save-settings-tip", "Save settings"), ("dont-show-again-tip", "Don't show this again"), + ("screenshot-merged-screen-not-supported-tip", "Merging screenshots of multiple displays is currently not supported. Please switch to a single display and try again."), + ("screenshot-action-tip", "Please select how to continue with the screenshot."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index a92c36449d8..2cf31126567 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 9c7528f025f..2e7a1d8230c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "La impresora {} está instalada y lista para usar."), ("Install {} Printer", "Instalar la impresora {}"), ("Outgoing Print Jobs", "Tareas salientes de impresión"), - ("Incomming Print Jobs", "Tareas entrantes de impresión"), + ("Incoming Print Jobs", "Tareas entrantes de impresión"), ("Incoming Print Job", "Trabajo entrante de impresión"), ("use-the-default-printer-tip", "Usar la impresora predeterminada"), ("use-the-selected-printer-tip", "Usar la impresora seleccionada"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "Los ajustes de permisos del lado controlado no permiten la impresión remota."), ("save-settings-tip", "Guardar ajustes"), ("dont-show-again-tip", "No volver a mostrar"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 2922711851f..501927ad8b9 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 851aa25fdd7..fd7f7407ff8 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 51bc3f4efbb..44bfc6754e2 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "چاپگر {} آماده است"), ("Install {} Printer", "نصب چاپگر {}"), ("Outgoing Print Jobs", "وظایف چاپ خروجی"), - ("Incomming Print Jobs", "وظایف چاپ ورودی"), + ("Incoming Print Jobs", "وظایف چاپ ورودی"), ("Incoming Print Job", "وظیفه چاپ ورودی"), ("use-the-default-printer-tip", "از چاپگر پیش‌فرض استفاده کنید"), ("use-the-selected-printer-tip", "از چاپگر انتخاب‌شده استفاده کنید"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "شما مجوز لازم برای چاپ از راه دور را ندارید"), ("save-settings-tip", "تنظیمات را ذخیره کنید"), ("dont-show-again-tip", "دیگر نمایش داده نشود"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index d599e653555..f15a762d452 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "L’imprimante {} est installée et opérationnelle."), ("Install {} Printer", "Installer l’imprimante {}"), ("Outgoing Print Jobs", "Impressions sortantes"), - ("Incomming Print Jobs", "Impressions entrantes"), + ("Incoming Print Jobs", "Impressions entrantes"), ("Incoming Print Job", "Impression entrante"), ("use-the-default-printer-tip", "Utiliser l’imprimante par défaut"), ("use-the-selected-printer-tip", "Utiliser l’imprimante sélectionnée"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "Les paramètres de l’appareil contrôlé n’autorisent pas l’impression à distance."), ("save-settings-tip", "Enregistrer les paramètres"), ("dont-show-again-tip", "Ne plus afficher"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 64863f28ac8..18d58a8c52f 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -239,7 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty", "ცარიელი"), ("Invalid folder name", "არასწორი საქაღალდის სახელი"), ("Socks5 Proxy", "SOCKS5-პროქსი"), - ("Proxy", "პროქსი"), + ("Socks5/Http(s) Proxy", ""), ("Discovered", "ნაპოვნია"), ("install_daemon_tip", "ჩატვირთვისას გასაშვებად საჭიროა სისტემური სერვისის დაყენება"), ("Remote ID", "დაშორებული ID"), @@ -567,7 +567,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("id_input_tip", "შეგიძლიათ შეიყვანოთ იდენტიფიკატორი, პირდაპირი 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"), - ("privacy_mode_impl_virtual_display_tip", "რეჟიმი 2"), ("Enter privacy mode", "კონფიდენციალურობის რეჟიმის ჩართვა"), ("Exit privacy mode", "კონფიდენციალურობის რეჟიმის გამორთვა"), ("idd_not_support_under_win10_2004_tip", "არაპირდაპირი ჩვენების დრაივერი არ არის მხარდაჭერილი. საჭიროა Windows 10 ვერსია 2004 ან უფრო ახალი."), @@ -659,7 +658,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "ხელმისაწვდომია ახალი ვერსია {}"), ("Accessible devices", "ხელმისაწვდომი მოწყობილობები"), ("View camera", "კამერის ნახვა"), - ("upgrade_remote_rustdesk_client_to{}_tip", "განაახლეთ RustDesk კლიენტი ვერსიამდე {} ან უფრო ახალი დისტანციურ მხარეზე!"), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), ("view_camera_unsupported_tip", "დისტანციური მოწყობილობა არ უჭერს მხარს კამერის ნახვას."), ("Enable camera", "კამერის ჩართვა"), ("No cameras", "კამერა არ არის"), @@ -672,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "პრინტერი {} დაინსტალირებულია და მზად არის გამოსაყენებლად."), ("Install {} Printer", "დააინსტალირეთ პრინტერი {}"), ("Outgoing Print Jobs", "გამავალი ბეჭდვის დავალება"), - ("Incomming Print Jobs", "შემომავალი ბეჭდვის დავალება"), + ("Incoming Print Jobs", "შემომავალი ბეჭდვის დავალება"), ("Incoming Print Job", "შემომავალი ბეჭდვის დავალება"), ("use-the-default-printer-tip", "ნაგულისხმევი პრინტერის გამოყენება"), ("use-the-selected-printer-tip", "არჩეული პრინტერის გამოყენება"), @@ -682,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "მართულ მხარეზე უფლებების პარამეტრები კრძალავს დისტანციურ ბეჭდვას."), ("save-settings-tip", "პარამეტრების შენახვა"), ("dont-show-again-tip", "აღარ აჩვენოთ"), - ].iter().cloned().collect(); + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index bdbeee14642..0d9e6d40c92 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 46c9795b48e..e76cb732e68 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index d297c9ed0f4..2db8390574d 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "A(z) {} nyomtató készen áll"), ("Install {} Printer", "A(z) {} nyomtató nyomtató telepítése"), ("Outgoing Print Jobs", "Kimenő nyomtatási feladatok"), - ("Incomming Print Jobs", "Bejövő nyomtatási feladatok"), + ("Incoming Print Jobs", "Bejövő nyomtatási feladatok"), ("Incoming Print Job", "Bejövő nyomtatási feladat"), ("use-the-default-printer-tip", "Alapértelmezett nyomtató használata"), ("use-the-selected-printer-tip", "Kiválasztott nyomtató használata"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "A távoli nyomtatás nincs engedélyezve"), ("save-settings-tip", "Beállítások mentése"), ("dont-show-again-tip", "Ne jelenítse meg újra"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f397442af97..06ca8002412 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 524c8625550..2f029ab5a4b 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "La stampante {} è installata e pronta all'uso."), ("Install {} Printer", "Installa la stampante {}"), ("Outgoing Print Jobs", "Lavori di stampa in uscita"), - ("Incomming Print Jobs", "Lavori di stampa in entrata"), + ("Incoming Print Jobs", "Lavori di stampa in entrata"), ("Incoming Print Job", "Lavoro di stampa in entrata"), ("use-the-default-printer-tip", "Usa la stampante predefinita"), ("use-the-selected-printer-tip", "Usa la stampante selezionata"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "Le impostazioni di autorizzazione del lato controllato negano la stampa remota."), ("save-settings-tip", "Salva impostazioni"), ("dont-show-again-tip", "Non visualizzare più questo messaggio"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a18d6d8e68a..bbfe4bb99d8 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index d104dd452d5..741864abd0b 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 82b095e503e..a755e77e0d3 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 07bbf2ac951..acaccd777dc 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 8601f48fdd0..23f3079588f 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "Printeris {} ir instalēts un gatavs lietošanai."), ("Install {} Printer", "Instalēt {} printeri"), ("Outgoing Print Jobs", "Izejošie drukas darbi"), - ("Incomming Print Jobs", "Ienākošie drukas darbi"), + ("Incoming Print Jobs", "Ienākošie drukas darbi"), ("Incoming Print Job", "Ienākošais drukas darbs"), ("use-the-default-printer-tip", "Izmantot noklusējuma printeri"), ("use-the-selected-printer-tip", "Izmantot atlasīto printeri"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "Kontrolētās puses atļauju iestatījumi liedz attālo drukāšanu."), ("save-settings-tip", "Saglabāt iestatījumus"), ("dont-show-again-tip", "Nerādīt šo vēlreiz"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index a6d909a9b48..96cd0682d0d 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 2c906ea9ed9..d9b14aff771 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "De printer {} is geïnstalleerd en klaar voor gebruik."), ("Install {} Printer", "Installeer {} Printer"), ("Outgoing Print Jobs", "Uitgaande Afdruktaken"), - ("Incomming Print Jobs", "Inkomende Afdruktaken"), + ("Incoming Print Jobs", "Inkomende Afdruktaken"), ("Incoming Print Job", "Inkomende Afdruktaak"), ("use-the-default-printer-tip", "Gebruik de standaard printer"), ("use-the-selected-printer-tip", "Gebruik de geselecteerde printer"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "Machtigingsinstellingen aan beheerde zijde verhinderen afdrukken op afstand."), ("save-settings-tip", "Instellingen opslaan"), ("dont-show-again-tip", "Dit bericht wordt niet meer weergegeven"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 70a3e25a9d3..016fbc1a30d 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "Drukarka {} jest zainstalowana i gotowa do użycia."), ("Install {} Printer", "Zainstaluj drukarkę {}"), ("Outgoing Print Jobs", "Wychodzące zadania drukowania"), - ("Incomming Print Jobs", "Przychodzące zadania drukowania"), + ("Incoming Print Jobs", "Przychodzące zadania drukowania"), ("Incoming Print Job", "Przychodzące zadanie drukowania"), ("use-the-default-printer-tip", "Użyj domyślnej drukarki"), ("use-the-selected-printer-tip", "Użyj wybranej drukarki"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "Ustawienia uprawnień po zdalnej stronie uniemożliwiają zdalne drukowanie."), ("save-settings-tip", "Zapisz ustawienia"), ("dont-show-again-tip", "Nie pokazuj więcej"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 155c8e16189..94977165758 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index dc8faa18ac0..f239d72a8ea 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "A impressora {} está instalada e operacional."), ("Install {} Printer", "Instalar impressora {}"), ("Outgoing Print Jobs", "Trabalhos de impressão enviados"), - ("Incomming Print Jobs", "Trabalhos de impressão recebidos"), + ("Incoming Print Jobs", "Trabalhos de impressão recebidos"), ("Incoming Print Job", "Impressão recebida"), ("use-the-default-printer-tip", "Usar impressora padrão"), ("use-the-selected-printer-tip", "Usar impressora selecionada"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "As configurações do dispositivo controlado não permitem impressão remota."), ("save-settings-tip", "Salvar configurações"), ("dont-show-again-tip", "Não mostrar novamente"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 9703a4d1560..5d074a77acc 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index d9322bfe12e..f970cc8cffe 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "Принтер {} установлен и готов к использованию."), ("Install {} Printer", "Установить принтер {}"), ("Outgoing Print Jobs", "Исходящее задание печати"), - ("Incomming Print Jobs", "Входящее задание печати"), + ("Incoming Print Jobs", "Входящее задание печати"), ("Incoming Print Job", "Входящее задание печати"), ("use-the-default-printer-tip", "Использовать принтер по умолчанию"), ("use-the-selected-printer-tip", "Использовать выбранный принтер"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "Настройки разрешений на управляемой стороне запрещают удалённую печать."), ("save-settings-tip", "Сохранить настройки"), ("dont-show-again-tip", "Больше не показывать"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 71e31e29852..ede1a4bf9f7 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "S'imprentadora {} est installada e pronta pro s'impreu."), ("Install {} Printer", "Installa s'imprentadora {}"), ("Outgoing Print Jobs", "Traballos de imprenta in essida"), - ("Incomming Print Jobs", "Traballos de imprenta in intrada"), + ("Incoming Print Jobs", "Traballos de imprenta in intrada"), ("Incoming Print Job", "Traballu de imprenta in intrada"), ("use-the-default-printer-tip", "Imprea s'imprentadora predefinida"), ("use-the-selected-printer-tip", "Imprea s'imprentadora seletzionada"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "Sas impostatziones de sos permissos de s'ala controllada negant s'imprenta remota."), ("save-settings-tip", "Sarva sas impostatziones"), ("dont-show-again-tip", "Non mustres prus custu messàgiu"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 9f96cb60ec0..95f336f1c34 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 405208f52ec..6156d3eb9ad 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index b1467f661bd..4564f1757f8 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 8e469d9be68..b482c154e12 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 5d3135719fc..8c3e28e5df6 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index d4d1760901e..53a6485043c 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 064a2f657e6..7df9025edd1 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 010bd3c2e44..f46237fd2db 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 1e36b85fdc5..7645a979872 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index cecdf1d05fa..0d863fd8d06 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", "{} 印表機已安裝,您可以使用列印功能了。"), ("Install {} Printer", "安裝 {} 印表機"), ("Outgoing Print Jobs", "傳出的列印任務"), - ("Incomming Print Jobs", "傳入的列印任務"), + ("Incoming Print Jobs", "傳入的列印任務"), ("Incoming Print Job", "傳入的列印任務"), ("use-the-default-printer-tip", "使用預設的印表機"), ("use-the-selected-printer-tip", "使用選取的印表機"), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "被控端的權限設置拒絕了遠端列印。"), ("save-settings-tip", "儲存設定"), ("dont-show-again-tip", "不再顯示此訊息"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 475dd071c89..8c4f9b6a5ea 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index e4462749523..defc5c03030 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -671,7 +671,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), ("Outgoing Print Jobs", ""), - ("Incomming Print Jobs", ""), + ("Incoming Print Jobs", ""), ("Incoming Print Job", ""), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), @@ -681,5 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 46e19315db6..b7d5be863f8 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2819,6 +2819,16 @@ impl Connection { Some(message::Union::VoiceCallResponse(_response)) => { // TODO: Maybe we can do a voice call from cm directly. } + Some(message::Union::ScreenshotRequest(request)) => { + if let Some(tx) = self.inner.tx.clone() { + crate::video_service::set_take_screenshot( + request.display as _, + request.sid.clone(), + tx, + ); + self.refresh_video_display(Some(request.display as usize)); + } + } _ => {} } } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 50831653553..a9474db7445 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -49,7 +49,7 @@ use scrap::{ codec::{Encoder, EncoderCfg}, record::{Recorder, RecorderContext}, vpxcodec::{VpxEncoderConfig, VpxVideoCodecId}, - CodecFormat, Display, EncodeInput, TraitCapturer, + CodecFormat, Display, EncodeInput, TraitCapturer, TraitPixelBuffer, }; #[cfg(windows)] use std::sync::Once; @@ -70,6 +70,13 @@ lazy_static::lazy_static! { pub static ref VIDEO_QOS: Arc> = Default::default(); pub static ref IS_UAC_RUNNING: Arc> = Default::default(); pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc> = Default::default(); + static ref SCREENSHOTS: Mutex> = Default::default(); +} + +struct Screenshot { + sid: String, + tx: Sender, + restore_vram: bool, } #[inline] @@ -457,7 +464,7 @@ fn get_capturer( } fn run(vs: VideoService) -> ResultType<()> { - let _raii = Raii::new(vs.sp.name()); + let mut _raii = Raii::new(vs.sp.name()); // Wayland only support one video capturer for now. It is ok to call ensure_inited() here. // // ensure_inited() is needed because clear() may be called. @@ -639,6 +646,49 @@ fn run(vs: VideoService) -> ResultType<()> { Ok(frame) => { repeat_encode_counter = 0; if frame.valid() { + let screenshot = SCREENSHOTS.lock().unwrap().remove(&display_idx); + if let Some(mut screenshot) = screenshot { + let restore_vram = screenshot.restore_vram; + let (msg, w, h, data) = match &frame { + scrap::Frame::PixelBuffer(f) => match get_rgba_from_pixelbuf(f) { + Ok(rgba) => ("".to_owned(), f.width(), f.height(), rgba), + Err(e) => { + let serr = e.to_string(); + log::error!( + "Failed to convert the pix format into rgba, {}", + &serr + ); + (format!("Convert pixfmt: {}", serr), 0, 0, vec![]) + } + }, + scrap::Frame::Texture(_) => { + if restore_vram { + // Already set one time, just ignore to break infinite loop. + // Though it's unreachable, this branch is kept to avoid infinite loop. + ( + "Please change codec and try again.".to_owned(), + 0, + 0, + vec![], + ) + } else { + #[cfg(all(windows, feature = "vram"))] + VRamEncoder::set_not_use(sp.name(), true); + screenshot.restore_vram = true; + SCREENSHOTS.lock().unwrap().insert(display_idx, screenshot); + _raii.try_vram = false; + bail!("SWITCH"); + } + } + }; + std::thread::spawn(move || { + handle_screenshot(screenshot, msg, w, h, data); + }); + if restore_vram { + bail!("SWITCH"); + } + } + let frame = frame.to(encoder.yuvfmt(), &mut yuv, &mut mid_data)?; let send_conn_ids = handle_one_frame( display_idx, @@ -764,24 +814,32 @@ fn run(vs: VideoService) -> ResultType<()> { Ok(()) } -struct Raii(String); +struct Raii { + name: String, + try_vram: bool, +} impl Raii { fn new(name: String) -> Self { log::info!("new video service: {}", name); VIDEO_QOS.lock().unwrap().new_display(name.clone()); - Raii(name) + Raii { + name, + try_vram: true, + } } } impl Drop for Raii { fn drop(&mut self) { - log::info!("stop video service: {}", self.0); + log::info!("stop video service: {}", self.name); #[cfg(feature = "vram")] - VRamEncoder::set_not_use(self.0.clone(), false); + if self.try_vram { + VRamEncoder::set_not_use(self.name.clone(), false); + } #[cfg(feature = "vram")] Encoder::update(scrap::codec::EncodingUpdate::Check); - VIDEO_QOS.lock().unwrap().remove_display(&self.0); + VIDEO_QOS.lock().unwrap().remove_display(&self.name); } } @@ -1206,3 +1264,77 @@ fn check_qos( drop(video_qos); Ok(()) } + +pub fn set_take_screenshot(display_idx: usize, sid: String, tx: Sender) { + SCREENSHOTS.lock().unwrap().insert( + display_idx, + Screenshot { + sid, + tx, + restore_vram: false, + }, + ); +} + +// We need to this function, because the `stride` may be larger than `width * 4`. +fn get_rgba_from_pixelbuf<'a>(pixbuf: &scrap::PixelBuffer<'a>) -> ResultType> { + let w = pixbuf.width(); + let h = pixbuf.height(); + let stride = pixbuf.stride(); + let Some(s) = stride.get(0) else { + bail!("Invalid pixel buf stride.") + }; + + if *s == w * 4 { + let mut rgba = vec![]; + scrap::convert(pixbuf, scrap::Pixfmt::RGBA, &mut rgba)?; + Ok(rgba) + } else { + let bgra = pixbuf.data(); + let mut bit_flipped = Vec::with_capacity(w * h * 4); + for y in 0..h { + for x in 0..w { + let i = s * y + 4 * x; + bit_flipped.extend_from_slice(&[bgra[i + 2], bgra[i + 1], bgra[i], bgra[i + 3]]); + } + } + Ok(bit_flipped) + } +} + +fn handle_screenshot(screenshot: Screenshot, msg: String, w: usize, h: usize, data: Vec) { + let mut response = ScreenshotResponse::new(); + response.sid = screenshot.sid; + if msg.is_empty() { + if data.is_empty() { + response.msg = "Failed to take screenshot, please try again later.".to_owned(); + } else { + fn encode_png(width: usize, height: usize, rgba: Vec) -> ResultType> { + let mut png = Vec::new(); + let mut encoder = + repng::Options::smallest(width as _, height as _).build(&mut png)?; + encoder.write(&rgba)?; + encoder.finish()?; + Ok(png) + } + match encode_png(w as _, h as _, data) { + Ok(png) => { + response.data = png.into(); + } + Err(e) => { + response.msg = format!("Error encoding png: {}", e); + } + } + } + } else { + response.msg = msg; + } + let mut msg_out = Message::new(); + msg_out.set_screenshot_response(response); + if let Err(e) = screenshot + .tx + .send((hbb_common::tokio::time::Instant::now(), Arc::new(msg_out))) + { + log::error!("Failed to send screenshot, {}", e); + } +} diff --git a/src/ui/header.tis b/src/ui/header.tis index 241cab0f9ff..13bd98de505 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -230,6 +230,7 @@ class Header: Reactor.Component { {restart_enabled && (pi.platform == "Linux" || pi.platform == "Windows" || pi.platform == "Mac OS") ?
  • {translate('Restart remote device')}
  • : ""} {keyboard_enabled ?
  • {translate('Insert Lock')}
  • : ""} {keyboard_enabled && pi.platform == "Windows" && pi.sas_enabled ?
  • {translate("Block user input")}
  • : ""} + {handler.is_screenshot_supported() ?
  • {translate('Take screenshot')}
  • : "" }
  • {translate('Refresh')}
  • ; @@ -376,6 +377,10 @@ class Header: Reactor.Component { event click $(#lock-screen) { handler.lock_screen(); } + + event click $(#take-screenshot) { + handler.take_screenshot(pi.current_display, ""); + } event click $(#refresh) { // 0 is just a dummy value. It will be ignored by the handler. @@ -546,6 +551,26 @@ handler.setCurrentDisplay = function(v) { } } +handler.screenshot = function(msg) { + if (msg) { + msgbox( + "custom-nocancel-nook-hasclose-error", + translate("Take screenshot"), + msg, + "", + function() {} + ); + } else { + msgbox( + "custom-take-screenshot-nocancel-nook", + translate("Take screenshot"), + translate("screenshot-action-tip"), + "", + function() {} + ); + } +} + function updatePrivacyMode() { var el = $(li#privacy-mode); if (el) { diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index da557e31222..542691f5fd7 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -132,6 +132,17 @@ class MsgboxComponent: Reactor.Component { return this.type.indexOf("skip") >= 0; } + function getScreenshotButtons() { + var isScreenshot = this.type.indexOf("take-screenshot") >= 0; + return isScreenshot + ?
    + + + +
    + : ""; + } + function render() { this.set_outline_focus(); var color = this.getColor(); @@ -170,6 +181,7 @@ class MsgboxComponent: Reactor.Component { {hasOk || this.hasRetry ? : ""} {hasLink ? : ""} {hasClose ? : ""} + {this.getScreenshotButtons()}
    @@ -245,6 +257,39 @@ class MsgboxComponent: Reactor.Component { this.close(); } } + + event click $(button#screenshotSaveAs) { + this.close(); + + handler.leave(handler.get_keyboard_mode()); + const filter = "Png file (*.png)"; + const defaultExt = "png"; + const initialPath = System.path(#USER_DOCUMENTS, "screenshot"); + const caption = "Save as"; + var url = view.selectFile(#save, filter, defaultExt, initialPath, caption); + handler.enter(handler.get_keyboard_mode()); + if(url) { + var res = handler.handle_screenshot("0:" + URL.toPath(url)); + if (res) { + msgbox("custom-error-nocancel-nook-hasclose", "Take screenshot", res, "", function() {}); + } + } else { + handler.handle_screenshot("2"); + } + } + + event click $(button#screenshotCopyToClip) { + this.close(); + var res = handler.handle_screenshot("1"); + if (res) { + msgbox("custom-error-nocancel-nook-hasclose", "Take screenshot", res, "", function() {}); + } + } + + event click $(button#screenshotCancel) { + this.close(); + handler.handle_screenshot("2"); + } event keydown (evt) { if (!evt.shortcutKey) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index fffc51b86f1..82cac19b18a 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -383,6 +383,10 @@ impl InvokeUiSession for SciterHandler { fn printer_request(&self, id: i32, path: String) { self.call("printerRequest", &make_args!(id, path)); } + + fn handle_screenshot_resp(&self, _sid: String, msg: String) { + self.call("screenshot", &make_args!(msg)); + } } pub struct SciterSession(Session); @@ -529,6 +533,9 @@ impl sciter::EventHandler for SciterSession { fn save_custom_image_quality(i32); fn refresh_video(i32); fn record_screen(bool); + fn is_screenshot_supported(); + fn take_screenshot(i32, String); + fn handle_screenshot(String); fn get_toggle_option(String); fn is_privacy_mode_supported(); fn toggle_option(String); @@ -866,6 +873,10 @@ impl SciterSession { fn on_printer_selected(&self, id: i32, path: String, printer_name: String) { self.printer_response(id, path, printer_name); } + + fn handle_screenshot(&self, action: String) -> String { + crate::client::screenshot::handle_screenshot(action) + } } pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index fb06493fba3..b3809466d40 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -412,6 +412,14 @@ impl Session { self.send(Data::RecordScreen(start)); } + pub fn is_screenshot_supported(&self) -> bool { + crate::common::is_support_screenshot_num(self.lc.read().unwrap().version) + } + + pub fn take_screenshot(&self, display: i32, sid: String) { + self.send(Data::TakeScreenshot((display, sid))); + } + pub fn is_recording(&self) -> bool { self.lc.read().unwrap().record_state } @@ -1586,6 +1594,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn update_record_status(&self, start: bool); fn update_empty_dirs(&self, _res: ReadEmptyDirsResponse) {} fn printer_request(&self, id: i32, path: String); + fn handle_screenshot_resp(&self, sid: String, msg: String); } impl Deref for Session { From 2ad1c907b8a0336dfe9261e914e159e1372568ef Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 30 Apr 2025 17:34:17 +0800 Subject: [PATCH 222/506] feat: hostname as id (#11605) Signed-off-by: fufesou --- Cargo.lock | 1 + libs/hbb_common | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 1afa949f1af..e3427f13bd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3111,6 +3111,7 @@ dependencies = [ "tungstenite", "url", "uuid", + "whoami", "winapi 0.3.9", "zstd 0.13.1", ] diff --git a/libs/hbb_common b/libs/hbb_common index 42aad01a517..bfddd5bb193 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 42aad01a517d4de7420ddcb7e99c29bd9f6d2b5a +Subproject commit bfddd5bb193883237c2a1c2c8d8e3cfe02823ac7 From df847e9a6066e8cfd360a2beaccd0b2f528af956 Mon Sep 17 00:00:00 2001 From: Shahar Naveh <50263213+ShaharNaveh@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:19:57 +0200 Subject: [PATCH 223/506] Add some hebrew translation (#11490) * Update he.rs * Update he.rs * Update he.rs * Update he.rs * Update he.rs --------- Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- src/lang/he.rs | 376 ++++++++++++++++++++++++------------------------- 1 file changed, 188 insertions(+), 188 deletions(-) diff --git a/src/lang/he.rs b/src/lang/he.rs index 0d9e6d40c92..894d0c619f7 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -6,12 +6,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("desk_tip", "ניתן לגשת לשולחן העבודה שלך עם מזהה וסיסמה זו."), ("Password", "סיסמא"), ("Ready", "מוכן"), - ("Established", ""), + ("Established", "הוקם"), ("connecting_status", "מתחבר לרשת RustDesk..."), - ("Enable service", ""), + ("Enable service", "הפעל שירות"), ("Start service", "התחל שירות"), ("Service is running", "השירות פעיל"), - ("Service is not running", ""), + ("Service is not running", "השירות איננו רץ"), ("not_ready_status", "לא מוכן. בדוק את החיבור שלך"), ("Control Remote Desktop", "שלוט בשולחן עבודה מרוחק"), ("Transfer file", "העבר קובץ"), @@ -19,144 +19,144 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recent sessions", "הפעלות אחרונות"), ("Address book", "ספר כתובות"), ("Confirmation", "אישור"), - ("TCP tunneling", ""), + ("TCP tunneling", "מנהור TCP"), ("Remove", "הסר"), ("Refresh random password", "רענן סיסמה אקראית"), ("Set your own password", "הגדר סיסמה משלך"), ("Enable keyboard/mouse", "אפשר מקלדת/עכבר"), ("Enable clipboard", "אפשר לוח גזירים"), ("Enable file transfer", "אפשר העברת קבצים"), - ("Enable TCP tunneling", ""), + ("Enable TCP tunneling", "הפעל מנהור TCP"), ("IP Whitelisting", "רשימת IP מורשים"), ("ID/Relay Server", "שרת זיהוי וממסר"), - ("Import server config", ""), - ("Export Server Config", ""), - ("Import server configuration successfully", ""), - ("Export server configuration successfully", ""), - ("Invalid server configuration", ""), - ("Clipboard is empty", ""), + ("Import server config", "ייבוא קונפיגורצית שרת"), + ("Export Server Config", "ייצוא קונפיגורצית שרת"), + ("Import server configuration successfully", "ייבוא קונפיגורצית הושלם בהצלחה"), + ("Export server configuration successfully", "ייצוא קונפיגורצית שרת הושלם בהצלחה"), + ("Invalid server configuration", "קונפיגורצית שרת לא תקינה"), + ("Clipboard is empty", "הקליפבורד ריק"), ("Stop service", "עצור שירות"), ("Change ID", "שנה מזהה"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "המזהה החדש שלך"), + ("length %min% to %max%", "אורך בין %min% ל %max%"), + ("starts with a letter", "מתחיל באות"), + ("allowed characters", "תווים מותרים"), ("id_change_tip", "מותרים רק תווים a-z, A-Z, 0-9, - (dash) ו_ (קו תחתון). האות הראשונה חייבת להיות a-z, A-Z. אורך בין 6 ל-16."), ("Website", "דף הבית"), ("About", "אודות"), ("Slogan_tip", "נוצר בלב בעולם הכאוטי הזה!"), - ("Privacy Statement", ""), + ("Privacy Statement", "הצהרת פרטיות"), ("Mute", "השתק"), ("Build Date", "תאריך בנייה"), ("Version", "גרסה"), ("Home", "בית"), ("Audio Input", "קלט שמע"), - ("Enhancements", ""), + ("Enhancements", "שיפורים"), ("Hardware Codec", "Codec חומרה"), - ("Adaptive bitrate", ""), + ("Adaptive bitrate", "קצב סיביות משתנה"), ("ID Server", "מזהה שרת"), - ("Relay Server", "שרת ריליי"), + ("Relay Server", "שרת ממסר (Relay)"), ("API Server", "שרת API"), ("invalid_http", "חייב להתחיל עם http:// או https://"), - ("Invalid IP", ""), - ("Invalid format", ""), + ("Invalid IP", "IP לא תקין"), + ("Invalid format", "פורמט לא תקין"), ("server_not_support", "עדיין לא נתמך על ידי השרת"), - ("Not available", ""), - ("Too frequent", ""), + ("Not available", "לא זמין"), + ("Too frequent", "תדיר מידיי"), ("Cancel", "ביטול"), ("Skip", "דלג"), ("Close", "סגור"), - ("Retry", "נזה שוב"), + ("Retry", "נסה שוב"), ("OK", "אישור"), ("Password Required", "נדרשת סיסמה"), - ("Please enter your password", ""), - ("Remember password", ""), + ("Please enter your password", "אנא הכנס סיסמה"), + ("Remember password", "זכור סיסמה"), ("Wrong Password", "סיסמה שגויה"), ("Do you want to enter again?", ""), ("Connection Error", "שגיאת חיבור"), ("Error", "שגיאה"), - ("Reset by the peer", ""), - ("Connecting...", ""), - ("Connection in progress. Please wait.", ""), - ("Please try 1 minute later", ""), + ("Reset by the peer", "איפוס על-ידי הצד השני"), + ("Connecting...", "מתחבר..."), + ("Connection in progress. Please wait.", "מתחבר. אנא המתן."), + ("Please try 1 minute later", "אנא נסה שוב בעוד דקה"), ("Login Error", "שגיאת התחברות"), - ("Successful", ""), - ("Connected, waiting for image...", ""), + ("Successful", "הצלחה"), + ("Connected, waiting for image...", "מחובר, מחכה לתמונה..."), ("Name", "שם"), ("Type", "סוג"), - ("Modified", ""), + ("Modified", "שונה"), ("Size", "גודל"), ("Show Hidden Files", "הצג קבצים נסתרים"), - ("Receive", ""), + ("Receive", "קיבל"), ("Send", "שלח"), ("Refresh File", "רענן קובץ"), ("Local", "מקומי"), - ("Remote", ""), + ("Remote", "מרוחק"), ("Remote Computer", "מחשב מרוחק"), ("Local Computer", "מחשב מקומי"), ("Confirm Delete", "אשר מחיקה"), ("Delete", "מחק"), - ("Properties", ""), + ("Properties", "תכונות"), ("Multi Select", "בחירה מרובה"), ("Select All", "בחר הכל"), ("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?", ""), + ("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!", ""), - ("Deleting", ""), + ("This is irreversible!", "בלתי הפיך"), + ("Deleting", "מוחק"), ("files", "קבצים"), - ("Waiting", ""), + ("Waiting", "מחכה"), ("Finished", "הסתיים"), ("Speed", "מהירות"), ("Custom Image Quality", "איכות תמונה מותאמת אישית"), - ("Privacy mode", ""), - ("Block user input", ""), - ("Unblock user input", ""), + ("Privacy mode", "מצב פרטיות"), + ("Block user input", "חסום קלט משתמש"), + ("Unblock user input", "אפשר קלט משתמש"), ("Adjust Window", "התאם חלון"), - ("Original", ""), - ("Shrink", ""), - ("Stretch", ""), + ("Original", "מקורי"), + ("Shrink", "הקטן"), + ("Stretch", "מתח"), ("Scrollbar", ""), ("ScrollAuto", ""), - ("Good image quality", ""), - ("Balanced", ""), + ("Good image quality", "איכות תמונה טובה"), + ("Balanced", "מאוזן"), ("Optimize reaction time", ""), ("Custom", "מותאם אישית"), - ("Show remote cursor", ""), + ("Show remote cursor", "הראה מצביע מרוחק"), ("Show quality monitor", ""), - ("Disable clipboard", ""), + ("Disable clipboard", "בטל את הקליפבורד"), ("Lock after session end", ""), - ("Insert Ctrl + Alt + Del", ""), + ("Insert Ctrl + Alt + Del", "לחץ Ctrl + Alt + Delete"), ("Insert Lock", "הוסף נעילה"), ("Refresh", "רענן"), - ("ID does not exist", ""), + ("ID does not exist", "מזהה אינו קיים"), ("Failed to connect to rendezvous server", ""), - ("Please try later", ""), + ("Please try later", "אנא נסה שוב מאוחר יותר"), ("Remote desktop is offline", ""), - ("Key mismatch", ""), + ("Key mismatch", "מפתח אינו תואם"), ("Timeout", ""), - ("Failed to connect to relay server", ""), + ("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", ""), + ("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", ""), + ("Click to upgrade", "לחץ כדי לשדרג"), + ("Click to download", "לחץ כדי להוריד"), + ("Click to update", "לחץ כדי לעדכן"), + ("Configure", "הגדר"), ("config_acc", "כדי לשלוט מרחוק בשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"נגישות\"."), ("config_screen", "כדי לגשת מרחוק לשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"הקלטת מסך\"."), - ("Installing ...", ""), + ("Installing ...", "מתקין ..."), ("Install", "התקן"), - ("Installation", ""), + ("Installation", "התקנה"), ("Installation Path", "נתיב התקנה"), - ("Create start menu shortcuts", ""), + ("Create start menu shortcuts", "צור קיצור-דרך לתפריט ההתחלה"), ("Create desktop icon", ""), ("agreement_tip", "על ידי התחלת ההתקנה, אתה מקבל את הסכם הרישיון."), ("Accept and Install", "קבל והתקן"), @@ -164,43 +164,43 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Generating ...", "יוצר ..."), ("Your installation is lower version.", ""), ("not_close_tcp_tip", "אל תסגור חלון זה בזמן שאתה משתמש במנהרה"), - ("Listening ...", ""), + ("Listening ...", "מאזין ..."), ("Remote Host", "מארח מרוחק"), ("Remote Port", "פורט מרוחק"), - ("Action", ""), + ("Action", "פעולה"), ("Add", "הוסף"), ("Local Port", "פורט מקומי"), ("Local Address", "כתובת מקומית"), ("Change Local Port", "שנה פורט מקומי"), ("setup_server_tip", "לחיבור מהיר יותר, אנא הגדר שרת משלך"), - ("Too short, at least 6 characters.", ""), - ("The confirmation is not identical.", ""), - ("Permissions", ""), - ("Accept", ""), + ("Too short, at least 6 characters.", "קצר מידיי, לפחות 6 תווים."), + ("The confirmation is not identical.", "האימות אינו זהה."), + ("Permissions", "הרשאות"), + ("Accept", "קבל"), ("Dismiss", ""), - ("Disconnect", ""), - ("Enable file copy and paste", ""), - ("Connected", ""), - ("Direct and encrypted connection", ""), + ("Disconnect", "נתק"), + ("Enable file copy and paste", "אפשר העתקה והדבקה עבור קבצים"), + ("Connected", "מחובר"), + ("Direct and encrypted connection", "חיבור ישיר ומוצפן"), ("Relayed and encrypted connection", ""), - ("Direct and unencrypted connection", ""), + ("Direct and unencrypted connection", "חיבור ישיר ולא מוצפן"), ("Relayed and unencrypted connection", ""), ("Enter Remote ID", "הזן מזהה מרוחק"), - ("Enter your password", ""), + ("Enter your password", "הכנס סיסמה"), ("Logging in...", ""), ("Enable RDP session sharing", ""), ("Auto Login", "התחברות אוטומטית (תקפה רק אם הגדרת \"נעל לאחר סיום הסשן\")"), ("Enable direct IP access", ""), ("Rename", "שנה שם"), - ("Space", ""), + ("Space", "רווח"), ("Create desktop shortcut", ""), ("Change Path", "שנה נתיב"), ("Create Folder", "צור תיקייה"), - ("Please enter the folder name", ""), - ("Fix it", ""), - ("Warning", ""), + ("Please enter the folder name", "אנא הכנס שם תיקייה"), + ("Fix it", "תקן את זה"), + ("Warning", "אזהרה"), ("Login screen using Wayland is not supported", ""), - ("Reboot required", ""), + ("Reboot required", "נדרש אתחול מחדש"), ("Unsupported display server", ""), ("x11 expected", ""), ("Port", "יציאה"), @@ -209,26 +209,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid port", ""), ("Closed manually by the peer", ""), ("Enable remote configuration modification", ""), - ("Run without install", ""), - ("Connect via relay", ""), - ("Always connect via relay", ""), + ("Run without install", "הרץ ללא התקנה"), + ("Connect via relay", "התחבר דרך ממסר"), + ("Always connect via relay", "תמיד תתחבר דרך ממסר"), ("whitelist_tip", "רק IP ברשימה הלבנה יכול לגשת אלי"), ("Login", "התחברות"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), + ("Verify", "אמת"), + ("Remember me", "זכור אותי"), + ("Trust this device", "סמוך על מכשיר זה"), ("Verification code", ""), ("verification_tip", "קוד אימות נשלח לכתובת הדוא\"ל הרשומה, הזן את קוד האימות כדי להמשיך בהתחברות."), - ("Logout", ""), + ("Logout", "התנתק"), ("Tags", "תגים"), ("Search ID", "חפש מזהה"), ("whitelist_sep", "מופרד על ידי פסיק, נקודה פסיק, רווחים או שורה חדשה"), ("Add ID", "הוסף מזהה"), ("Add Tag", "הוסף תג"), ("Unselect all tags", ""), - ("Network error", ""), - ("Username missed", ""), - ("Password missed", ""), + ("Network error", "שגיאת רשת"), + ("Username missed", "חסר שם משתמש"), + ("Password missed", "חסרה סיסמה"), ("Wrong credentials", "שם משתמש או סיסמה שגויים"), ("The verification code is incorrect or has expired", ""), ("Edit Tag", "ערוך תג"), @@ -237,18 +237,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Favorites", "הוסף למועדפים"), ("Remove from Favorites", "הסר מהמועדפים"), ("Empty", "ריק"), - ("Invalid folder name", ""), + ("Invalid folder name", "שם תיקייה אינו תקין"), ("Socks5 Proxy", "פרוקסי Socks5"), ("Socks5/Http(s) Proxy", "פרוקסי Socks5/Http(s)"), ("Discovered", ""), ("install_daemon_tip", "לצורך הפעלה בעת הפעלת המחשב, עליך להתקין שירות מערכת."), - ("Remote ID", ""), + ("Remote ID", "מזהה מרוחק"), ("Paste", "הדבק"), - ("Paste here?", ""), + ("Paste here?", "להסביר כאן?"), ("Are you sure to close the connection?", "האם אתה בטוח שברצונך לסגור את החיבור?"), - ("Download new version", ""), - ("Touch mode", ""), - ("Mouse mode", ""), + ("Download new version", "הורד גרסה חדשה"), + ("Touch mode", "מצב מגע"), + ("Mouse mode", "מצב עכבר"), ("One-Finger Tap", "הקשה באצבע אחת"), ("Left Mouse", "עכבר שמאלי"), ("One-Long Tap", "הקשה ארוכה באצבע אחת"), @@ -266,18 +266,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reset canvas", ""), ("No permission of file transfer", ""), ("Note", "הערה"), - ("Connection", ""), + ("Connection", "התחברות"), ("Share Screen", "שיתוף מסך"), ("Chat", "צ'אט"), ("Total", "הכל"), - ("items", ""), - ("Selected", ""), + ("items", "פריטים"), + ("Selected", "נבחר"), ("Screen Capture", "לכידת מסך"), ("Input Control", "בקרת קלט"), ("Audio Capture", "לכידת שמע"), ("File Connection", "חיבור קובץ"), ("Screen Connection", "חיבור מסך"), - ("Do you accept?", ""), + ("Do you accept?", "האם אתה מקבל?"), ("Open System Setting", "פתח הגדרת מערכת"), ("How to get Android input permission?", ""), ("android_input_permission_tip1", "כדי שמכשיר מרוחק יוכל לשלוט במכשיר האנדרואיד שלך באמצעות עכבר או מגע, עליך לאפשר ל-RustDesk להשתמש בשירות \"נגישות\"."), @@ -288,7 +288,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "גרסת האנדרואיד הנוכחית אינה תומכת בלכידת שמע, אנא שדרג לאנדרואיד 10 או גבוה יותר."), ("android_start_service_tip", "הקש על [התחל שירות] או אפשר הרשאת [לכידת מסך] כדי להתחיל את שירות שיתוף המסך."), ("android_permission_may_not_change_tip", "הרשאות עבור חיבורים שנוצרו עשויות לא להשתנות מייד עד להתחברות מחדש."), - ("Account", ""), + ("Account", "חשבון"), ("Overwrite", ""), ("This file exists, skip or overwrite this file?", ""), ("Quit", "צא"), @@ -296,22 +296,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed", "נכשל"), ("Succeeded", "הצליח"), ("Someone turns on privacy mode, exit", ""), - ("Unsupported", ""), + ("Unsupported", "לא נתמח"), ("Peer denied", ""), ("Please install plugins", ""), ("Peer exit", ""), - ("Failed to turn off", ""), - ("Turned off", ""), + ("Failed to turn off", "הכיבוי נכשל"), + ("Turned off", "מכובה"), ("Language", "שפה"), ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", "התעלם מאופטימיזציות סוללה"), ("android_open_battery_optimizations_tip", "אם ברצונך לבטל תכונה זו, אנא עבור לדף ההגדרות של יישום RustDesk הבא, מצא והכנס ל[סוללה], הסר את הסימון מ-[לא מוגבל]"), - ("Start on boot", ""), + ("Start on boot", "התחל בהפעלה"), ("Start the screen sharing service on boot, requires special permissions", ""), - ("Connection not allowed", ""), + ("Connection not allowed", "חיבור לא מורשה"), ("Legacy mode", ""), ("Map mode", ""), - ("Translate mode", ""), + ("Translate mode", "מצב תרגום"), ("Use permanent password", "השתמש בסיסמה קבועה"), ("Use both passwords", "השתמש בשתי הסיסמאות"), ("Set permanent password", "הגדר סיסמה קבועה"), @@ -327,7 +327,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select Monitor", "בחר מסך"), ("Control Actions", "פעולות בקרה"), ("Display Settings", "הגדרות תצוגה"), - ("Ratio", ""), + ("Ratio", "יחס"), ("Image Quality", "איכות תמונה"), ("Scroll Style", "סגנון גלילה"), ("Show Toolbar", "הצג סרגל כלים"), @@ -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"), @@ -375,7 +375,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Prompt", "הנחיה"), ("Please wait for confirmation of UAC...", ""), ("elevated_foreground_window_tip", "החלון הנוכחי של שולחן העבודה המרוחק דורש הרשאה גבוהה יותר לפעולה, לכן אי אפשר להשתמש בעכבר ובמקלדת באופן זמני. תוכל לבקש מהמשתמש המרוחק למזער את החלון הנוכחי, או ללחוץ על כפתור ההגבהה בחלון ניהול החיבור. כדי להימנע מבעיה זו, מומלץ להתקין את התוכנה במכשיר המרוחק."), - ("Disconnected", ""), + ("Disconnected", "מנותק"), ("Other", "אחר"), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", "הגדרות מקלדת"), @@ -388,7 +388,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", ""), ("This PC", "מחשב זה"), ("or", "או"), - ("Continue with", ""), + ("Continue with", "המשך עם"), ("Elevate", ""), ("Zoom cursor", ""), ("Accept sessions via password", ""), @@ -396,20 +396,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("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", ""), + ("Select local keyboard type", "בחר סוג מקלדת מקומי"), ("software_render_tip", "אם אתה משתמש בכרטיס גרפיקה של Nvidia תחת Linux וחלון המרחוק נסגר מיד לאחר החיבור, החלפה למנהל ההתקן הפתוח Nouveau ובחירה בשימוש בעיבוד תוכנה עשויה לעזור. נדרשת הפעלה מחדש של התוכנה."), ("Always use software rendering", ""), ("config_input", "כדי לשלוט בשולחן העבודה המרוחק באמצעות מקלדת, עליך להעניק ל-RustDesk הרשאות \"מעקב אחרי קלט\"."), @@ -426,12 +426,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Elevate successfully", ""), ("uppercase", ""), ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("digit", "ספרה"), + ("special character", "תו מיוחד"), + ("length>=8", "לפחות באורך 8"), + ("Weak", "חלש"), + ("Medium", "בינוני"), + ("Strong", "חזק"), ("Switch Sides", "החלף צדדים"), ("Please confirm if you want to share your desktop?", ""), ("Display", "תצוגה"), @@ -439,23 +439,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Default Scroll Style", "סגנון גלילה ברירת מחדל"), ("Default Image Quality", "איכות תמונה ברירת מחדל"), ("Default Codec", "קודק ברירת מחדל"), - ("Bitrate", ""), + ("Bitrate", "קצב סיביות"), ("FPS", "FPS"), ("Auto", "אוטומטי"), ("Other Default Options", "אפשרויות ברירת מחדל אחרות"), ("Voice call", "שיחה קולית"), ("Text chat", ""), - ("Stop voice call", ""), + ("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", "מיין לפי"), ("New Connection", "חיבור חדש"), - ("Restore", ""), + ("Restore", "שיחזור"), ("Minimize", "הקטן"), ("Maximize", "הגדל"), ("Your Device", "המכשיר שלך"), @@ -463,10 +463,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "עדיין אין עמיתים מועדפים?\nבוא נמצא מישהו להתחבר אליו ונוסיף אותו למועדפים!"), ("empty_lan_tip", "אוי לא, נראה שעדיין לא גילינו עמיתים."), ("empty_address_book_tip", "אוי ואבוי, נראה שכרגע אין עמיתים בספר הכתובות שלך."), - ("eg: admin", ""), + ("eg: admin", "לדוגמא: admin"), ("Empty Username", "שם משתמש ריק"), ("Empty Password", "סיסמה ריקה"), - ("Me", ""), + ("Me", "אני"), ("identical_file_tip", "קובץ זה זהה לקובץ של העמית."), ("show_monitors_tip", "הצג מסכים בסרגל כלים"), ("View Mode", "מצב תצוגה"), @@ -485,7 +485,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("System Sound", "צליל מערכת"), ("Default", "ברירת מחדל"), ("New RDP", "RDP חדש"), - ("Fingerprint", ""), + ("Fingerprint", "טביעת אצבע"), ("Copy Fingerprint", "העתק טביעת אצבע"), ("no fingerprints", "אין טביעות אצבע"), ("Select a peer", ""), @@ -503,8 +503,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept and Elevate", "קבל והגבה"), ("accept_and_elevate_btn_tooltip", "קבל את החיבור והגבה הרשאות UAC."), ("clipboard_wait_response_timeout_tip", "המתנה לתגובת העתקה הסתיימה בזמן."), - ("Incoming connection", ""), - ("Outgoing connection", ""), + ("Incoming connection", "חיבור נכנס"), + ("Outgoing connection", "חיבור יוצא"), ("Exit", "צא"), ("Open", "פתח"), ("logout_tip", "האם אתה בטוח שברצונך להתנתק?"), @@ -532,13 +532,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Primary Color", "צבע עיקרי"), ("HSV Color", "צבע HSV"), ("Installation Successful!", "ההתקנה הצליחה!"), - ("Installation failed!", ""), + ("Installation failed!", "התקנה נכשלה!"), ("Reverse mouse wheel", ""), ("{} sessions", ""), ("scam_title", "ייתכן שאתה נפלת להונאה!"), ("scam_text1", "אם אתה בשיחת טלפון עם מישהו שאינך מכיר ואינך סומך עליו שביקש ממך להשתמש ב-RustDesk ולהתחיל את השירות, אל תמשיך ונתק מיד."), ("scam_text2", "סביר להניח שמדובר בהונאה שמנסה לגנוב ממך כסף או מידע פרטי אחר."), - ("Don't show again", ""), + ("Don't show again", "אל תראה שוב"), ("I Agree", "אני מסכים"), ("Decline", "דחה"), ("Timeout in minutes", ""), @@ -551,24 +551,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remove wallpaper during incoming sessions", ""), ("Test", "בדיקה"), ("display_is_plugged_out_msg", "המסך הופסק, החלף למסך הראשון."), - ("No displays", ""), + ("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", ""), + ("Big tiles", "כותרות גדולות"), + ("Small tiles", "כותרות קטנות"), ("List", "רשימה"), - ("Virtual display", ""), - ("Plug out all", ""), + ("Virtual display", "מסכים ווירטואלים"), + ("Plug out all", "נתק הכל"), ("True color (4:4:4)", ""), ("Enable blocking user input", ""), ("id_input_tip", "ניתן להזין מזהה, 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"), @@ -580,50 +580,50 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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.", ""), + ("Email verification code must be 6 characters.", "קוד אימות במייל חייב להיות באורך של 6 תווים."), + ("2FA code must be 6 digits.", "קוד אימות דו-שלבי חייב להיות באורך של 6 מספרים."), ("Multiple Windows sessions found", ""), ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("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", ""), + ("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", ""), + ("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", "חלון צף"), ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), + ("Keep screen on", "השאר מסך דולק"), + ("Never", "אף פעם"), + ("During controlled", "בזמן שליטה"), + ("During service is on", "בזמן שהשירות מופעל"), ("Capture screen using DirectX", ""), ("Back", "חזור"), ("Apps", "אפליקציות"), - ("Volume up", ""), - ("Volume down", ""), + ("Volume up", "הגבר"), + ("Volume down", "הנמך"), ("Power", ""), ("Telegram bot", ""), ("enable-bot-tip", ""), @@ -633,46 +633,46 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About RustDesk", "אודות RustDesk"), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), - ("Unlock with PIN", ""), + ("Unlock with PIN", "שחרר עם PIN"), ("Requires at least {} characters", ""), - ("Wrong PIN", ""), + ("Wrong PIN", "PIN שגוי"), ("Set PIN", "הגדר PIN"), ("Enable trusted devices", ""), ("Manage trusted devices", ""), ("Platform", "פלטורמה"), - ("Days remaining", ""), + ("Days remaining", "ימים שנשארו"), ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("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", ""), + ("Upload folder", "העלה תקייה"), + ("Upload files", "העלה קבצים"), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), - ("Untagged", ""), + ("Untagged", "לא מתוייג"), ("new-version-of-{}-tip", ""), - ("Accessible devices", ""), - ("View camera", ""), + ("Accessible devices", "מכשירים נגישים"), + ("View camera", "צפה במצלמה"), ("upgrade_remote_rustdesk_client_to_{}_tip", "אנא שדרג את לקוח RustDesk לגרסה {} או חדשה יותר בצד המרוחק!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), + ("Enable camera", "הפעל מצלמה"), + ("No cameras", "אין מצלמות"), ("d3d_render_tip", ""), ("Use D3D rendering", ""), - ("Printer", ""), + ("Printer", "מדפסת"), ("printer-os-requirement-tip", ""), ("printer-requires-installed-{}-client-tip", ""), ("printer-{}-not-installed-tip", ""), ("printer-{}-ready-tip", ""), ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incoming Print Jobs", ""), - ("Incoming Print Job", ""), + ("Outgoing Print Jobs", "עבודות הדפסה יוצאות"), + ("Incoming Print Jobs", "עבודות הדפסה נכנסות"), + ("Incoming Print Job", "עבודת הדפסה נכנסת"), ("use-the-default-printer-tip", ""), ("use-the-selected-printer-tip", ""), ("auto-print-tip", ""), From 83aae23ba648dfae4fb95d1f359b38bd94cd67c9 Mon Sep 17 00:00:00 2001 From: solokot Date: Wed, 30 Apr 2025 17:21:54 +0300 Subject: [PATCH 224/506] Update ru.rs (#11608) --- src/lang/ru.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index f970cc8cffe..22ca95124c5 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -681,11 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "Настройки разрешений на управляемой стороне запрещают удалённую печать."), ("save-settings-tip", "Сохранить настройки"), ("dont-show-again-tip", "Больше не показывать"), - ("Take screenshot", ""), - ("Taking screenshot", ""), - ("screenshot-merged-screen-not-supported-tip", ""), - ("screenshot-action-tip", ""), - ("Save as", ""), - ("Copy to clipboard", ""), + ("Take screenshot", "Сделать снимок экрана"), + ("Taking screenshot", "Получение снимка экрана"), + ("screenshot-merged-screen-not-supported-tip", "Объединение снимков экранов с нескольких дисплеев в настоящее время не поддерживается. Переключитесь на один дисплей и повторите действие."), + ("screenshot-action-tip", "Выберите, что делать с полученным снимком экрана."), + ("Save as", "Сохранить в файл"), + ("Copy to clipboard", "Копировать в буфер обмена"), ].iter().cloned().collect(); } From 20fcddffbd7d7f4e0c3648e18f5c35be8df407e7 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 30 Apr 2025 23:49:18 +0800 Subject: [PATCH 225/506] fix: build (#11611) Signed-off-by: fufesou --- Cargo.lock | 1 - Cargo.toml | 7 +++---- libs/hbb_common | 2 +- src/client.rs | 2 +- src/clipboard.rs | 2 +- src/common.rs | 2 ++ src/lan.rs | 4 ++-- src/platform/linux.rs | 2 +- src/server/connection.rs | 10 +++++----- src/ui_session_interface.rs | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3427f13bd9..a5a187595b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5947,7 +5947,6 @@ dependencies = [ "uuid", "virtual_display", "wallpaper", - "whoami", "winapi 0.3.9", "windows 0.61.1", "windows-service", diff --git a/Cargo.toml b/Cargo.toml index ff0e8a8701f..6cf0e5b865f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,6 @@ screencapturekit = ["cpal/screencapturekit"] [dependencies] async-trait = "0.1" -whoami = "1.5.0" scrap = { path = "libs/scrap", features = ["wayland"] } hbb_common = { path = "libs/hbb_common" } serde_derive = "1.0" @@ -95,7 +94,7 @@ sys-locale = "0.3" enigo = { path = "libs/enigo", features = [ "with_serde" ] } clipboard = { path = "libs/clipboard" } ctrlc = "3.2" -# arboard = { version = "3.4.0", features = ["wayland-data-control"] } +# arboard = { version = "3.4", features = ["wayland-data-control"] } arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] } clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" } @@ -118,7 +117,7 @@ winapi = { version = "0.3", features = [ "ioapiset", "winspool", ] } -windows = { version = "0.61.1", features = [ +windows = { version = "0.61", features = [ "Win32", "Win32_System", "Win32_System_Diagnostics", @@ -131,7 +130,7 @@ virtual_display = { path = "libs/virtual_display" } remote_printer = { path = "libs/remote_printer" } impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" } shared_memory = "0.12" -tauri-winrt-notification = "0.1.2" +tauri-winrt-notification = "0.1" runas = "1.2" [target.'cfg(target_os = "macos")'.dependencies] diff --git a/libs/hbb_common b/libs/hbb_common index bfddd5bb193..4eca5b45b95 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit bfddd5bb193883237c2a1c2c8d8e3cfe02823ac7 +Subproject commit 4eca5b45b95b2f3d71789dc001595149c015fe72 diff --git a/src/client.rs b/src/client.rs index dbad64675d7..137701c54a5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2335,7 +2335,7 @@ impl LoginConfigHandler { .collect::>() .join(" "); #[cfg(not(target_os = "android"))] - let my_platform = whoami::platform().to_string(); + let my_platform = hbb_common::whoami::platform().to_string(); #[cfg(target_os = "android")] let my_platform = "Android".into(); let hwid = if self.get_option("trust-this-device") == "Y" { diff --git a/src/clipboard.rs b/src/clipboard.rs index ee976d68fd8..3ab95e41e75 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -463,7 +463,7 @@ pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bo if get_version_number(peer_version) < get_version_number("1.3.0") { return false; } - if ["", &whoami::Platform::Ios.to_string()].contains(&peer_platform) { + if ["", &hbb_common::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") diff --git a/src/common.rs b/src/common.rs index f8d5571b2f9..264fe81cab3 100644 --- a/src/common.rs +++ b/src/common.rs @@ -7,6 +7,8 @@ use std::{ use serde_json::{json, Map, Value}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::whoami; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, diff --git a/src/lan.rs b/src/lan.rs index 7d3f4f05fed..f2f370587f1 100644 --- a/src/lan.rs +++ b/src/lan.rs @@ -1,7 +1,7 @@ -use hbb_common::config::Config; use hbb_common::{ allow_err, anyhow::bail, + config::Config, config::{self, RENDEZVOUS_PORT}, log, protobuf::Message as _, @@ -10,7 +10,7 @@ use hbb_common::{ self, sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, }, - ResultType, + whoami, ResultType, }; use std::{ diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 5c1a1cf2c3b..46592017724 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1089,7 +1089,7 @@ mod desktop { } self.display = self .display - .replace(&whoami::hostname(), "") + .replace(&hbb_common::whoami::hostname(), "") .replace("localhost", ""); } diff --git a/src/server/connection.rs b/src/server/connection.rs index b7d5be863f8..9edefcb84f8 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1268,8 +1268,8 @@ impl Connection { #[cfg(not(target_os = "android"))] { - pi.hostname = whoami::hostname(); - pi.platform = whoami::platform().to_string(); + pi.hostname = hbb_common::whoami::hostname(); + pi.platform = hbb_common::whoami::platform().to_string(); } #[cfg(target_os = "android")] { @@ -1314,13 +1314,13 @@ impl Connection { #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] { let is_both_windows = cfg!(target_os = "windows") - && self.lr.my_platform == whoami::Platform::Windows.to_string(); + && self.lr.my_platform == hbb_common::whoami::Platform::Windows.to_string(); #[cfg(feature = "unix-file-copy-paste")] let is_unix_and_peer_supported = crate::is_support_file_copy_paste(&self.lr.version); #[cfg(not(feature = "unix-file-copy-paste"))] let is_unix_and_peer_supported = false; let is_both_macos = cfg!(target_os = "macos") - && self.lr.my_platform == whoami::Platform::MacOS.to_string(); + && self.lr.my_platform == hbb_common::whoami::Platform::MacOS.to_string(); let is_peer_support_paste_if_macos = crate::is_support_file_paste_if_macos(&self.lr.version); let has_file_clipboard = is_both_windows @@ -4279,7 +4279,7 @@ mod raii { ) -> Self { let printer = conn_type == crate::server::AuthConnType::Remote && crate::is_support_remote_print(&lr.version) - && lr.my_platform == whoami::Platform::Windows.to_string(); + && lr.my_platform == hbb_common::whoami::Platform::Windows.to_string(); AUTHED_CONNS.lock().unwrap().push(AuthedConn { conn_id, conn_type, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index b3809466d40..dbadd0747c1 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -29,7 +29,7 @@ use hbb_common::{ sync::mpsc, time::{Duration as TokioDuration, Instant}, }, - Stream, + whoami, Stream, }; use crate::client::io_loop::Remote; From d30ead1d969945800b84ff2e33c1c2c06046e341 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Thu, 1 May 2025 20:55:26 +0200 Subject: [PATCH 226/506] Italian language update (#11619) --- 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 2f029ab5a4b..3f1fd7e1adc 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -681,11 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "Le impostazioni di autorizzazione del lato controllato negano la stampa remota."), ("save-settings-tip", "Salva impostazioni"), ("dont-show-again-tip", "Non visualizzare più questo messaggio"), - ("Take screenshot", ""), - ("Taking screenshot", ""), - ("screenshot-merged-screen-not-supported-tip", ""), - ("screenshot-action-tip", ""), - ("Save as", ""), - ("Copy to clipboard", ""), + ("Take screenshot", "Cattura schermata"), + ("Taking screenshot", "Cattura schermata"), + ("screenshot-merged-screen-not-supported-tip", "L'unione della cattura di schermate di più display non è attualmente supportata.\nPassa ad un singolo display e riprova."), + ("screenshot-action-tip", "Seleziona come continuare con la schermata."), + ("Save as", "Salva come"), + ("Copy to clipboard", "Copia negli appunti"), ].iter().cloned().collect(); } From ec1de6413a59b39962e8de82e08e56f6f2c0db47 Mon Sep 17 00:00:00 2001 From: summoner001 Date: Thu, 1 May 2025 18:55:36 +0000 Subject: [PATCH 227/506] Update hu.rs (#11620) * Update hu.rs Translate strings and fixing * Update hu.rs fix sentence * Update hu.rs fix sentence --- src/lang/hu.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 2db8390574d..82ecefd2f55 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -76,12 +76,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection Error", "Kapcsolódási hiba"), ("Error", "Hiba"), ("Reset by the peer", "A kapcsolatot a másik fél lezárta."), - ("Connecting...", "Kapcsolódás…"), + ("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...", "Kapcsolódva, várakozás a képadatokra…"), + ("Connected, waiting for image…", "Kapcsolódva, várakozás a képadatokra…"), ("Name", "Név"), ("Type", "Típus"), ("Modified", "Módosított"), @@ -152,7 +152,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Configure", "Beállítás"), ("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…"), + ("Installing …", "Telepítés…"), ("Install", "Telepítés"), ("Installation", "Telepítés"), ("Installation Path", "Telepítési útvonal"), @@ -161,10 +161,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "A telepítés folytatásával automatikusan elfogadásra kerül a licensz szerződés."), ("Accept and Install", "Elfogadás és telepítés"), ("End-user license agreement", "Végfelhasználói licensz szerződés"), - ("Generating ...", "Létrehozás…"), + ("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, amíg TCP-alagutat használ"), - ("Listening ...", "Figyelés…"), + ("Listening …", "Figyelés…"), ("Remote Host", "Távoli kiszolgáló"), ("Remote Port", "Távoli port"), ("Action", "Indítás"), @@ -187,7 +187,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", "Adja meg a jelszavát"), - ("Logging in...", "Belépés folyamatban…"), + ("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"), @@ -373,7 +373,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN discovery", "Felfedezés tiltása"), ("Write a message", "Üzenet írása"), ("Prompt", "Kérés"), - ("Please wait for confirmation of UAC...", "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", "Kapcsolat bontva"), ("Other", "Egyéb"), @@ -394,7 +394,7 @@ 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...", "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"), @@ -681,11 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "A távoli nyomtatás nincs engedélyezve"), ("save-settings-tip", "Beállítások mentése"), ("dont-show-again-tip", "Ne jelenítse meg újra"), - ("Take screenshot", ""), - ("Taking screenshot", ""), - ("screenshot-merged-screen-not-supported-tip", ""), - ("screenshot-action-tip", ""), - ("Save as", ""), - ("Copy to clipboard", ""), + ("Take screenshot", "Képernyőkép készítése"), + ("Taking screenshot", "Képernyőkép készítése…"), + ("screenshot-merged-screen-not-supported-tip", "Egyesített képernyőről nem támogatott a képernyőkép készítése"), + ("screenshot-action-tip", "Képernyőkép-művelet"), + ("Save as", "Mentés másként"), + ("Copy to clipboard", "Másolás a vágólapra"), ].iter().cloned().collect(); } From 7196dbed6e51c2ab0b03b0f7311651f263df2c88 Mon Sep 17 00:00:00 2001 From: XLion Date: Fri, 2 May 2025 02:55:50 +0800 Subject: [PATCH 228/506] Update tw.rs (#11615) --- src/lang/tw.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0d863fd8d06..9ecd3ba93cd 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -681,11 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "被控端的權限設置拒絕了遠端列印。"), ("save-settings-tip", "儲存設定"), ("dont-show-again-tip", "不再顯示此訊息"), - ("Take screenshot", ""), - ("Taking screenshot", ""), - ("screenshot-merged-screen-not-supported-tip", ""), - ("screenshot-action-tip", ""), - ("Save as", ""), - ("Copy to clipboard", ""), + ("Take screenshot", "擷取畫面"), + ("Taking screenshot", "正在擷取畫面"), + ("screenshot-merged-screen-not-supported-tip", "目前不支援合併多個螢幕的截圖。請切換至單一螢幕後再試。"), + ("screenshot-action-tip", "請選擇要如何處理這張截圖。"), + ("Save as", "另存為"), + ("Copy to clipboard", "複製到剪貼簿"), ].iter().cloned().collect(); } From 04e2792f5fcab82018acffd56476b3851f154854 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 2 May 2025 03:41:55 +0800 Subject: [PATCH 229/506] use tcp only for socks5 --- libs/hbb_common | 2 +- src/rendezvous_mediator.rs | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/libs/hbb_common b/libs/hbb_common index 4eca5b45b95..3afaf649447 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 4eca5b45b95b2f3d71789dc001595149c015fe72 +Subproject commit 3afaf6494475ef58dcaaae6b4e6d2303cc3d632b diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index d8ec3c102d5..f731a58ded3 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -383,14 +383,8 @@ impl RendezvousMediator { pub async fn start(server: ServerPtr, host: String) -> ResultType<()> { log::info!("start rendezvous mediator of {}", host); //If the investment agent type is http or https, then tcp forwarding is enabled. - let is_http_proxy = if let Some(conf) = Config::get_socks() { - let proxy = Proxy::from_conf(&conf, None)?; - proxy.is_http_or_https() - } else { - false - }; if (cfg!(debug_assertions) && option_env!("TEST_TCP").is_some()) - || is_http_proxy + || Config::is_proxy() || get_builtin_option(config::keys::OPTION_DISABLE_UDP) == "Y" { Self::start_tcp(server, host).await From 7c8d2daaf67a4cb43e1cc447ccaa5b8f6d898d95 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 2 May 2025 03:49:51 +0800 Subject: [PATCH 230/506] update lock --- Cargo.lock | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5a187595b4..49e6d5985f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3111,7 +3111,6 @@ dependencies = [ "tungstenite", "url", "uuid", - "whoami", "winapi 0.3.9", "zstd 0.13.1", ] @@ -7567,12 +7566,6 @@ dependencies = [ "wit-bindgen-rt", ] -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -7771,17 +7764,6 @@ dependencies = [ "rustix 0.38.34", ] -[[package]] -name = "whoami" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" -dependencies = [ - "redox_syscall 0.4.1", - "wasite", - "web-sys", -] - [[package]] name = "widestring" version = "1.1.0" From e55722308eddc87f2699dc130cde98d9c1eb4c0c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 2 May 2025 03:53:19 +0800 Subject: [PATCH 231/506] fix ci --- Cargo.lock | 18 ++++++++++++++++++ libs/hbb_common | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 49e6d5985f4..fcff3416c91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3111,6 +3111,7 @@ dependencies = [ "tungstenite", "url", "uuid", + "whoami", "winapi 0.3.9", "zstd 0.13.1", ] @@ -7566,6 +7567,12 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -7764,6 +7771,17 @@ dependencies = [ "rustix 0.38.34", ] +[[package]] +name = "whoami" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +dependencies = [ + "redox_syscall 0.5.2", + "wasite", + "web-sys", +] + [[package]] name = "widestring" version = "1.1.0" diff --git a/libs/hbb_common b/libs/hbb_common index 3afaf649447..4eca5b45b95 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 3afaf6494475ef58dcaaae6b4e6d2303cc3d632b +Subproject commit 4eca5b45b95b2f3d71789dc001595149c015fe72 From 62276b4f4f2e19d528176e1987b784a88c7a2e1a Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Fri, 2 May 2025 01:15:20 +0200 Subject: [PATCH 232/506] Update de.rs (#11627) --- src/lang/de.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 221b786ddf8..ed846762dab 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -681,11 +681,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "Die Berechtigungseinstellungen der kontrollierten Seite verweigern den entfernten Druck."), ("save-settings-tip", "Einstellungen speichern"), ("dont-show-again-tip", "Nicht mehr anzeigen"), - ("Take screenshot", ""), - ("Taking screenshot", ""), - ("screenshot-merged-screen-not-supported-tip", ""), - ("screenshot-action-tip", ""), - ("Save as", ""), - ("Copy to clipboard", ""), + ("Take screenshot", "Screenshot aufnehmen"), + ("Taking screenshot", "Screenshot aufnehmen …"), + ("screenshot-merged-screen-not-supported-tip", "Das Zusammenführen von Screenshots von mehreren Bildschirmen wird derzeit nicht unterstützt. Bitte wechseln Sie zu einem einzelnen Bildschirm und versuchen Sie es erneut."), + ("screenshot-action-tip", "Bitte wählen Sie aus, wie Sie mit dem Screenshot fortfahren möchten."), + ("Save as", "Speichern unter"), + ("Copy to clipboard", "In Zwischenablage kopieren"), ].iter().cloned().collect(); } From ca00706a38051e5e2c31c74d90d6685ab1860878 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 4 May 2025 07:32:47 +0800 Subject: [PATCH 233/506] feat, update, win, macos (#11618) Signed-off-by: fufesou --- flutter/lib/common.dart | 50 +- flutter/lib/consts.dart | 1 + .../lib/desktop/pages/desktop_home_page.dart | 20 +- .../desktop/pages/desktop_setting_page.dart | 11 +- .../lib/desktop/widgets/update_progress.dart | 234 ++++++++ res/msi/Package/Components/RustDesk.wxs | 2 +- .../Package/Fragments/AddRemoveProperties.wxs | 2 + src/common.rs | 6 +- src/core_main.rs | 40 +- src/flutter.rs | 13 +- src/flutter_ffi.rs | 106 +++- src/hbbs_http.rs | 1 + src/hbbs_http/downloader.rs | 274 ++++++++++ src/ipc.rs | 23 + src/lang/ar.rs | 7 + src/lang/be.rs | 7 + src/lang/bg.rs | 7 + src/lang/ca.rs | 7 + src/lang/cn.rs | 7 + src/lang/cs.rs | 7 + src/lang/da.rs | 7 + src/lang/de.rs | 7 + src/lang/el.rs | 7 + src/lang/en.rs | 3 + src/lang/eo.rs | 7 + src/lang/es.rs | 7 + src/lang/et.rs | 7 + src/lang/eu.rs | 7 + src/lang/fa.rs | 7 + src/lang/fr.rs | 7 + src/lang/ge.rs | 9 +- src/lang/he.rs | 7 + src/lang/hr.rs | 7 + src/lang/hu.rs | 23 +- src/lang/id.rs | 7 + src/lang/it.rs | 7 + src/lang/ja.rs | 7 + src/lang/ko.rs | 7 + src/lang/kz.rs | 7 + src/lang/lt.rs | 7 + src/lang/lv.rs | 7 + src/lang/nb.rs | 7 + src/lang/nl.rs | 7 + src/lang/pl.rs | 7 + src/lang/pt_PT.rs | 7 + src/lang/ptbr.rs | 7 + src/lang/ro.rs | 7 + src/lang/ru.rs | 7 + src/lang/sc.rs | 7 + src/lang/sk.rs | 7 + src/lang/sl.rs | 7 + src/lang/sq.rs | 7 + src/lang/sr.rs | 7 + src/lang/sv.rs | 7 + src/lang/ta.rs | 7 + src/lang/template.rs | 7 + src/lang/th.rs | 7 + src/lang/tr.rs | 7 + src/lang/tw.rs | 7 + src/lang/uk.rs | 7 + src/lang/vn.rs | 7 + src/lib.rs | 7 +- src/platform/macos.rs | 209 +++++++- src/platform/mod.rs | 71 ++- src/platform/privileges_scripts/install.scpt | 2 +- .../privileges_scripts/uninstall.scpt | 2 +- src/platform/privileges_scripts/update.scpt | 18 + src/platform/windows.cc | 7 +- src/platform/windows.rs | 501 +++++++++++++++++- src/rendezvous_mediator.rs | 4 + src/ui/index.tis | 1 + src/updater.rs | 249 +++++++++ 72 files changed, 2128 insertions(+), 69 deletions(-) create mode 100644 flutter/lib/desktop/widgets/update_progress.dart create mode 100644 src/hbbs_http/downloader.rs create mode 100644 src/platform/privileges_scripts/update.scpt create mode 100644 src/updater.rs diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index f972766623c..3088b3f2a78 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1152,15 +1152,23 @@ Widget createDialogContent(String text) { void msgBox(SessionID sessionId, String type, String title, String text, String link, OverlayDialogManager dialogManager, - {bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) { + {bool? hasCancel, + ReconnectHandle? reconnect, + int? reconnectTimeout, + VoidCallback? onSubmit, + int? submitTimeout}) { dialogManager.dismissAll(); List buttons = []; bool hasOk = false; submit() { dialogManager.dismissAll(); - // https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 - if (!type.contains("custom") && desktopType != DesktopType.portForward) { - closeConnection(); + if (onSubmit != null) { + onSubmit.call(); + } else { + // https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 + if (!type.contains("custom") && desktopType != DesktopType.portForward) { + closeConnection(); + } } } @@ -1176,7 +1184,18 @@ void msgBox(SessionID sessionId, String type, String title, String text, if (type != "connecting" && type != "success" && !type.contains("nook")) { hasOk = true; - buttons.insert(0, dialogButton('OK', onPressed: submit)); + late final Widget btn; + if (submitTimeout != null) { + btn = _CountDownButton( + text: 'OK', + second: submitTimeout, + onPressed: submit, + submitOnTimeout: true, + ); + } else { + btn = dialogButton('OK', onPressed: submit); + } + buttons.insert(0, btn); } hasCancel ??= !type.contains("error") && !type.contains("nocancel") && @@ -1197,7 +1216,8 @@ void msgBox(SessionID sessionId, String type, String title, String text, reconnectTimeout != null) { // `enabled` is used to disable the dialog button once the button is clicked. final enabled = true.obs; - final button = Obx(() => _ReconnectCountDownButton( + final button = Obx(() => _CountDownButton( + text: 'Reconnect', second: reconnectTimeout, onPressed: enabled.isTrue ? () { @@ -3183,21 +3203,24 @@ parseParamScreenRect(Map params) { get isInputSourceFlutter => stateGlobal.getInputSource() == "Input source 2"; -class _ReconnectCountDownButton extends StatefulWidget { - _ReconnectCountDownButton({ +class _CountDownButton extends StatefulWidget { + _CountDownButton({ Key? key, + required this.text, required this.second, required this.onPressed, + this.submitOnTimeout = false, }) : super(key: key); + final String text; final VoidCallback? onPressed; final int second; + final bool submitOnTimeout; @override - State<_ReconnectCountDownButton> createState() => - _ReconnectCountDownButtonState(); + State<_CountDownButton> createState() => _CountDownButtonState(); } -class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> { +class _CountDownButtonState extends State<_CountDownButton> { late int _countdownSeconds = widget.second; Timer? _timer; @@ -3218,6 +3241,9 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> { _timer = Timer.periodic(Duration(seconds: 1), (timer) { if (_countdownSeconds <= 0) { timer.cancel(); + if (widget.submitOnTimeout) { + widget.onPressed?.call(); + } } else { setState(() { _countdownSeconds--; @@ -3229,7 +3255,7 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> { @override Widget build(BuildContext context) { return dialogButton( - '${translate('Reconnect')} (${_countdownSeconds}s)', + '${translate(widget.text)} (${_countdownSeconds}s)', onPressed: widget.onPressed, isOutline: true, ); diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index a4d60b0a674..5ae15e52e62 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -139,6 +139,7 @@ const String kOptionCurrentAbName = "current-ab-name"; const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs"; const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render"; const String kOptionEnableCheckUpdate = "enable-check-update"; +const String kOptionAllowAutoUpdate = "allow-auto-update"; const String kOptionAllowLinuxHeadless = "allow-linux-headless"; const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper"; const String kOptionStopService = "stop-service"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 7f30a5a63d5..07421d14fae 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -12,6 +12,7 @@ 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/update_progress.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'; @@ -22,7 +23,6 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:window_manager/window_manager.dart'; import 'package:window_size/window_size.dart' as window_size; - import '../widgets/button.dart'; class DesktopHomePage extends StatefulWidget { @@ -433,13 +433,23 @@ class _DesktopHomePageState extends State updateUrl.isNotEmpty && !isCardClosed && bind.mainUriPrefixSync().contains('rustdesk')) { + final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled(); + String btnText = isToUpdate ? 'Click to update' : 'Click to download'; + GestureTapCallback onPressed = () async { + final Uri url = Uri.parse('https://rustdesk.com/download'); + await launchUrl(url); + }; + if (isToUpdate) { + onPressed = () { + handleUpdate(updateUrl); + }; + } return buildInstallCard( "Status", "${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); - }, closeButton: true); + btnText, + onPressed, + closeButton: true); } if (systemError.isNotEmpty) { return buildInstallCard("", systemError, "", () {}); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 5ac53775ce0..f81925c7725 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -470,6 +470,8 @@ class _GeneralState extends State<_General> { } Widget other() { + final showAutoUpdate = + isWindows && bind.mainIsInstalled() && !bind.isCustomClient(); final children = [ if (!isWeb && !bind.isIncomingOnly()) _OptionCheckBox(context, 'Confirm before closing multiple tabs', @@ -523,12 +525,19 @@ class _GeneralState extends State<_General> { kOptionEnableCheckUpdate, isServer: false, ), + if (showAutoUpdate) + _OptionCheckBox( + context, + 'Auto update', + kOptionAllowAutoUpdate, + isServer: true, + ), if (isWindows && !bind.isOutgoingOnly()) _OptionCheckBox( context, 'Capture screen using DirectX', kOptionDirectxCapture, - ) + ), ], ]; if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { diff --git a/flutter/lib/desktop/widgets/update_progress.dart b/flutter/lib/desktop/widgets/update_progress.dart new file mode 100644 index 00000000000..ac425fa2bda --- /dev/null +++ b/flutter/lib/desktop/widgets/update_progress.dart @@ -0,0 +1,234 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher.dart'; + +void handleUpdate(String releasePageUrl) { + String downloadUrl = releasePageUrl.replaceAll('tag', 'download'); + String version = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1); + final String downloadFile = + bind.mainGetCommonSync(key: 'download-file-$version'); + if (downloadFile.startsWith('error:')) { + final error = downloadFile.replaceFirst('error:', ''); + msgBox(gFFI.sessionId, 'custom-nocancel-nook-hasclose', 'Error', error, + releasePageUrl, gFFI.dialogManager); + return; + } + downloadUrl = '$downloadUrl/$downloadFile'; + + SimpleWrapper downloadId = SimpleWrapper(''); + SimpleWrapper onCanceled = SimpleWrapper(() {}); + gFFI.dialogManager.dismissAll(); + gFFI.dialogManager.show((setState, close, context) { + return CustomAlertDialog( + title: Text(translate('Downloading {$appName}')), + content: + UpdateProgress(releasePageUrl, downloadUrl, downloadId, onCanceled) + .marginSymmetric(horizontal: 8) + .paddingOnly(top: 12), + actions: [ + dialogButton(translate('Cancel'), onPressed: () async { + onCanceled.value(); + await bind.mainSetCommon( + key: 'cancel-downloader', value: downloadId.value); + // Wait for the downloader to be removed. + for (int i = 0; i < 10; i++) { + await Future.delayed(const Duration(milliseconds: 300)); + final isCanceled = 'error:Downloader not found' == + await bind.mainGetCommon( + key: 'download-data-${downloadId.value}'); + if (isCanceled) { + break; + } + } + close(); + }, isOutline: true), + ]); + }); +} + +class UpdateProgress extends StatefulWidget { + final String releasePageUrl; + final String downloadUrl; + final SimpleWrapper downloadId; + final SimpleWrapper onCanceled; + UpdateProgress( + this.releasePageUrl, this.downloadUrl, this.downloadId, this.onCanceled, + {Key? key}) + : super(key: key); + + @override + State createState() => UpdateProgressState(); +} + +class UpdateProgressState extends State { + Timer? _timer; + int? _totalSize; + int _downloadedSize = 0; + int _getDataFailedCount = 0; + final String _eventKeyDownloadNewVersion = 'download-new-version'; + + @override + void initState() { + super.initState(); + widget.onCanceled.value = () { + cancelQueryTimer(); + }; + platformFFI.registerEventHandler(_eventKeyDownloadNewVersion, + _eventKeyDownloadNewVersion, handleDownloadNewVersion, + replace: true); + bind.mainSetCommon(key: 'download-new-version', value: widget.downloadUrl); + } + + @override + void dispose() { + cancelQueryTimer(); + platformFFI.unregisterEventHandler( + _eventKeyDownloadNewVersion, _eventKeyDownloadNewVersion); + super.dispose(); + } + + void cancelQueryTimer() { + _timer?.cancel(); + _timer = null; + } + + Future handleDownloadNewVersion(Map evt) async { + if (evt.containsKey('id')) { + widget.downloadId.value = evt['id'] as String; + _timer = Timer.periodic(const Duration(milliseconds: 300), (timer) { + _updateDownloadData(); + }); + } else { + if (evt.containsKey('error')) { + _onError(evt['error'] as String); + } else { + // unreachable + _onError('$evt'); + } + } + } + + void _onError(String error) { + cancelQueryTimer(); + + debugPrint('Download new version error: $error'); + final msgBoxType = 'custom-nocancel-nook-hasclose'; + final msgBoxTitle = 'Error'; + final msgBoxText = 'download-new-version-failed-tip'; + final dialogManager = gFFI.dialogManager; + + close() { + dialogManager.dismissAll(); + } + + jumplink() { + launchUrl(Uri.parse(widget.releasePageUrl)); + dialogManager.dismissAll(); + } + + retry() { + dialogManager.dismissAll(); + handleUpdate(widget.releasePageUrl); + } + + final List buttons = [ + dialogButton('Download', onPressed: jumplink), + dialogButton('Retry', onPressed: retry), + dialogButton('Close', onPressed: close), + ]; + dialogManager.dismissAll(); + dialogManager.show( + (setState, close, context) => CustomAlertDialog( + title: null, + content: SelectionArea( + child: msgboxContent(msgBoxType, msgBoxTitle, msgBoxText)), + actions: buttons, + ), + tag: '$msgBoxType-$msgBoxTitle-$msgBoxTitle', + ); + } + + void _updateDownloadData() { + String err = ''; + String downloadData = + bind.mainGetCommonSync(key: 'download-data-${widget.downloadId.value}'); + if (downloadData.startsWith('error:')) { + err = downloadData.substring('error:'.length); + } else { + try { + jsonDecode(downloadData).forEach((key, value) { + if (key == 'total_size') { + if (value != null && value is int) { + _totalSize = value; + } + } else if (key == 'downloaded_size') { + _downloadedSize = value as int; + } else if (key == 'error') { + if (value != null) { + err = value.toString(); + } + } + }); + } catch (e) { + _getDataFailedCount += 1; + debugPrint( + 'Failed to get download data ${widget.downloadUrl}, error $e'); + if (_getDataFailedCount > 3) { + err = e.toString(); + } + } + } + if (err != '') { + _onError(err); + } else { + if (_totalSize != null && _downloadedSize >= _totalSize!) { + cancelQueryTimer(); + bind.mainSetCommon( + key: 'remove-downloader', value: widget.downloadId.value); + if (_totalSize == 0) { + _onError('The download file size is 0.'); + } else { + setState(() {}); + msgBox( + gFFI.sessionId, + 'custom-nocancel', + '{$appName} Update', + '{$appName}-to-update-tip', + '', + gFFI.dialogManager, + onSubmit: () { + debugPrint('Downloaded, update to new version now'); + bind.mainSetCommon(key: 'update-me', value: widget.downloadUrl); + }, + submitTimeout: 5, + ); + } + } else { + setState(() {}); + } + } + } + + @override + Widget build(BuildContext context) { + return onDownloading(context); + } + + Widget onDownloading(BuildContext context) { + final value = _totalSize == null + ? 0.0 + : (_totalSize == 0 ? 1.0 : _downloadedSize / _totalSize!); + return LinearProgressIndicator( + value: value, + minHeight: 20, + borderRadius: BorderRadius.circular(5), + backgroundColor: Colors.grey[300], + valueColor: const AlwaysStoppedAnimation(Colors.blue), + ); + } +} diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index ffe4f0ffbd6..337e84ec3c5 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -56,7 +56,7 @@ - + + + + diff --git a/res/msi/preprocess.py b/res/msi/preprocess.py index 670091ad8fb..cb2140d21bb 100644 --- a/res/msi/preprocess.py +++ b/res/msi/preprocess.py @@ -49,7 +49,7 @@ def make_parser(): "--dist-dir", type=str, default="../../rustdesk", - help="The dist direcotry to install.", + help="The dist directory to install.", ) parser.add_argument( "--arp", From 5dd15d128247865244e3e5000c4bc41c7441826e Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:25:15 +0800 Subject: [PATCH 355/506] fix: privacy mode, msgbox sometimes does not show (#12117) Signed-off-by: fufesou --- flutter/lib/models/model.dart | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d8d33048b9c..c28d3c1d637 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -991,17 +991,12 @@ class FfiModel with ChangeNotifier { String link, bool hasRetry, OverlayDialogManager dialogManager) { - if (text == 'no_need_privacy_mode_no_physical_displays_tip' || - text == 'Enter privacy mode') { - // There are display changes on the remote side, - // which will cause some messages to refresh the canvas and dismiss dialogs. - // So we add a delay here to ensure the dialog is displayed. - Future.delayed(Duration(milliseconds: 3000), () { - showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager); - }); - } else { + // There are display changes on the remote side, + // which will cause some messages to refresh the canvas and dismiss dialogs. + // So we add a delay here to ensure the dialog is displayed. + Future.delayed(Duration(milliseconds: 3000), () { showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager); - } + }); } _updateSessionWidthHeight(SessionID sessionId) { From 1eee03818d46a445d918624135642642748f5a35 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 19 Jun 2025 12:28:16 +0900 Subject: [PATCH 356/506] fix https://github.com/rustdesk/rustdesk/discussions/11838 --- .../com/carriez/flutter_hbb/InputService.kt | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 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 b2a827e129a..8ea67fe0b76 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 @@ -294,14 +294,31 @@ class InputService : AccessibilityService() { } try { if (stroke == null) { - stroke = GestureDescription.StrokeDescription( - touchPath, - 0, - duration, - willContinue - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + stroke = GestureDescription.StrokeDescription( + touchPath, + 0, + duration, + willContinue + ) + } else { + stroke = GestureDescription.StrokeDescription( + touchPath, + 0, + duration + ) + } } else { - stroke = stroke?.continueStroke(touchPath, 0, duration, willContinue) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + stroke = stroke?.continueStroke(touchPath, 0, duration, willContinue) + } else { + stroke = null + stroke = GestureDescription.StrokeDescription( + touchPath, + 0, + duration + ) + } } stroke?.let { val builder = GestureDescription.Builder() From 590ecc43ffe8f878079a293cb1e14228e89afaca Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 19 Jun 2025 13:21:47 +0900 Subject: [PATCH 357/506] try fix firefox v2 paste problem --- flutter/lib/models/web_model.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index 5241c3974ff..863f0f93d2f 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -128,9 +128,6 @@ class PlatformFFI { }; context.callMethod('init'); version = getByName('version'); - window.onContextMenu.listen((event) { - event.preventDefault(); - }); context['onRegisteredEvent'] = (String message) { try { From d6ba0636552ac166c216c28d585e0c0e221cfa38 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:39:15 +0800 Subject: [PATCH 358/506] fix: win, privacy mode 2 (#12123) * fix: win, privacy mode 2 Signed-off-by: fufesou * Update src/privacy_mode/win_virtual_display.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Signed-off-by: fufesou Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/core_main.rs | 2 +- src/platform/windows.rs | 21 +++--- src/privacy_mode.rs | 5 +- src/privacy_mode/win_virtual_display.rs | 90 ++++++++++++++++++++----- 4 files changed, 92 insertions(+), 26 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index eab98918136..cee6ac0b9fb 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -345,7 +345,7 @@ pub fn core_main() -> Option> { hbb_common::allow_err!(crate::run_me(vec!["--tray"])); } #[cfg(windows)] - crate::privacy_mode::restore_reg_connectivity(true); + crate::privacy_mode::restore_reg_connectivity(true, false); #[cfg(any(target_os = "linux", target_os = "windows"))] { crate::start_server(true, false); diff --git a/src/platform/windows.rs b/src/platform/windows.rs index b3aec8d83e7..15661b35522 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -3010,16 +3010,21 @@ pub mod reg_display_settings { None } - pub fn restore_reg_connectivity(reg_recovery: RegRecovery) -> ResultType<()> { + pub fn restore_reg_connectivity(reg_recovery: RegRecovery, force: bool) -> 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(()); + if !force { + 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), + }; + // Compare if the current value is the same as the new value. + // If they are not the same, the registry value has been changed by other processes. + // So we do not restore the registry value. + if cur_reg_value != new_reg_value { + return Ok(()); + } } let reg_value = RegValue { bytes: reg_recovery.old.0, diff --git a/src/privacy_mode.rs b/src/privacy_mode.rs index a02b8bc93eb..d96f639fdc2 100644 --- a/src/privacy_mode.rs +++ b/src/privacy_mode.rs @@ -219,9 +219,10 @@ async fn turn_on_privacy_async(impl_key: String, conn_id: i32) -> Option match res { Ok(res) => res, Err(e) => Some(Err(anyhow!(e.to_string()))), diff --git a/src/privacy_mode/win_virtual_display.rs b/src/privacy_mode/win_virtual_display.rs index d235575fdac..f521cbacb24 100644 --- a/src/privacy_mode/win_virtual_display.rs +++ b/src/privacy_mode/win_virtual_display.rs @@ -172,6 +172,7 @@ impl PrivacyModeImpl { } fn set_primary_display(&mut self) -> ResultType { + // Multiple virtual displays with different origins are tested. let display = &self.virtual_displays[0]; let display_name = std::string::String::from_utf16(&display.name)?; @@ -194,9 +195,32 @@ impl PrivacyModeImpl { ); } + // Windows 24H2 requires the virtual display to be set first. + // No idea why, maybe the same issue: https://developercommunity.visualstudio.com/t/Windows-11-Enterprise-24H2-using-WinApi/10851936?sort=newest + let flags = CDS_UPDATEREGISTRY | CDS_NORESET; + let offx = new_primary_dm.u1.s2().dmPosition.x; + let offy = new_primary_dm.u1.s2().dmPosition.y; + new_primary_dm.u1.s2_mut().dmPosition.x = 0; + new_primary_dm.u1.s2_mut().dmPosition.y = 0; + new_primary_dm.dmFields |= DM_POSITION; + let rc = ChangeDisplaySettingsExW( + display.name.as_ptr(), + &mut new_primary_dm, + NULL as _, + flags | CDS_SET_PRIMARY, + NULL, + ); + if rc != DISP_CHANGE_SUCCESSFUL { + let err = Self::change_display_settings_ex_err_msg(rc); + log::error!( + "Failed ChangeDisplaySettingsEx, the virtual display, {}", + &err + ); + bail!("Failed ChangeDisplaySettingsEx, {}", err); + } + let mut i: DWORD = 0; loop { - let mut flags = CDS_UPDATEREGISTRY | CDS_NORESET; #[allow(invalid_value)] let mut dd: DISPLAY_DEVICEW = std::mem::MaybeUninit::uninit().assume_init(); dd.cb = std::mem::size_of::() as _; @@ -209,9 +233,9 @@ impl PrivacyModeImpl { if (dd.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) == 0 { continue; } - + // Skip the virtual display. if dd.DeviceName == display.name { - flags |= CDS_SET_PRIMARY; + continue; } #[allow(invalid_value)] @@ -228,8 +252,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.u1.s2_mut().dmPosition.x -= offx; + dm.u1.s2_mut().dmPosition.y -= offy; dm.dmFields |= DM_POSITION; let rc = ChangeDisplaySettingsExW( dd.DeviceName.as_ptr(), @@ -263,6 +287,9 @@ impl PrivacyModeImpl { Ok(display_name) } + // NOTE: We can't detect if the other virtual displays are physical displays or not. + // We can only use `DeviceString` == `virtual_display_manager::get_cur_device_string()` to detect if the display is a virtual display. + // The other virtual displays can't be restored after exiting the privacy mode on Windows 24H2. fn disable_physical_displays(&self) -> ResultType<()> { for display in &self.displays { let mut dm = display.dm.clone(); @@ -303,21 +330,34 @@ impl PrivacyModeImpl { }] } - pub fn ensure_virtual_display(&mut self) -> ResultType<()> { + // This function will wait at most 6 seconds for the virtual displays to be ready. + // It's ok to wait, because: + // 1. A new thread is created to handle the async privacy mode. + // 2. The user is usually not in a hurry to turn on the privacy mode. + pub fn ensure_virtual_display(&mut self, is_async_mode: bool) -> ResultType<()> { if self.virtual_displays.is_empty() { let displays = virtual_display_manager::plug_in_peer_request(vec![Self::default_display_modes()])?; - if virtual_display_manager::is_amyuni_idd() { - thread::sleep(Duration::from_secs(3)); + if is_async_mode { + thread::sleep(Duration::from_secs(1)); } self.set_displays(); - // No physical displays, no need to use the privacy mode. if self.displays.is_empty() { virtual_display_manager::plug_out_monitor_indices(&displays, false, false)?; bail!(NO_PHYSICAL_DISPLAYS); } + if is_async_mode { + let now = std::time::Instant::now(); + while self.virtual_displays.is_empty() + && now.elapsed() < Duration::from_millis(5000) + { + thread::sleep(Duration::from_millis(500)); + self.set_displays(); + } + } + self.virtual_displays_added.extend(displays); } @@ -364,9 +404,22 @@ impl PrivacyModeImpl { 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(); + let is_virtual_display_added = self.virtual_displays_added.len() > 0; + if is_virtual_display_added { + self.restore_plug_out_monitor(); + } else { + // https://github.com/rustdesk/rustdesk/pull/12114#issuecomment-2983054370 + // No virtual displays added, we need to change the display combination to force the display settings to be reloaded. + // This function changes the user behavior of the virtual displays. + // But it makes the privacy mode more stable. + // No need to restore the virtual displays. It's easy to notice that the virtual displays are plugged out. + let _ = virtual_display_manager::plug_out_monitor(-1, true, false); + + // We can't replug the virtual dislays here. + // TODO: plug out + plug in the virtual displays (`IDD_IMPL_AMYUNI`) in a short time makes the server side crash. + } } fn restore_displays(displays: &[Display]) { @@ -418,12 +471,13 @@ impl PrivacyMode for PrivacyModeImpl { bail!(NO_PHYSICAL_DISPLAYS); } + let is_async_mode = self.is_async_privacy_mode(); let mut guard = TurnOnGuard { privacy_mode: self, succeeded: false, }; - guard.ensure_virtual_display()?; + guard.ensure_virtual_display(is_async_mode)?; if guard.virtual_displays.is_empty() { log::debug!("No virtual displays"); bail!("No virtual displays."); @@ -434,7 +488,11 @@ impl PrivacyMode for PrivacyModeImpl { 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)); + 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) = @@ -466,7 +524,9 @@ impl PrivacyMode for PrivacyModeImpl { super::win_input::unhook()?; let _tmp_ignore_changed_holder = crate::display_service::temp_ignore_displays_changed(); self.restore(); - restore_reg_connectivity(false); + // We need to force restore the registry connectivity. + // This is because the registry connection may be changed by `self.restore()`, but will not be fully restored. + restore_reg_connectivity(false, true); if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID { if let Some(state) = state { @@ -507,7 +567,7 @@ fn reset_config_reg_connectivity() { Config::set_option(CONFIG_KEY_REG_RECOVERY.to_owned(), "".to_owned()); } -pub fn restore_reg_connectivity(plug_out_monitors: bool) { +pub fn restore_reg_connectivity(plug_out_monitors: bool, force: bool) { let config_recovery_value = Config::get_option(CONFIG_KEY_REG_RECOVERY); if config_recovery_value.is_empty() { return; @@ -518,7 +578,7 @@ pub fn restore_reg_connectivity(plug_out_monitors: bool) { if let Ok(reg_recovery) = serde_json::from_str::(&config_recovery_value) { - if let Err(e) = reg_display_settings::restore_reg_connectivity(reg_recovery) { + if let Err(e) = reg_display_settings::restore_reg_connectivity(reg_recovery, force) { log::error!("Failed restore_reg_connectivity, error: {}", e); } } From 46cd090f985b1a092a76e3987c571e27714f7fb1 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 19 Jun 2025 22:31:40 +0800 Subject: [PATCH 359/506] Revert "try fix firefox v2 paste problem" (#12126) This reverts commit 590ecc43ffe8f878079a293cb1e14228e89afaca. --- flutter/lib/models/web_model.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index 863f0f93d2f..5241c3974ff 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -128,6 +128,9 @@ class PlatformFFI { }; context.callMethod('init'); version = getByName('version'); + window.onContextMenu.listen((event) { + event.preventDefault(); + }); context['onRegisteredEvent'] = (String message) { try { From 7330dc70f3705be0d8a6ce18236cec002b7dbc61 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 20 Jun 2025 17:50:28 +0800 Subject: [PATCH 360/506] fix: android 7.1, input, crash (#12129) Signed-off-by: fufesou --- .../com/carriez/flutter_hbb/InputService.kt | 56 +++++++++++++++---- .../com/carriez/flutter_hbb/MainActivity.kt | 2 +- 2 files changed, 46 insertions(+), 12 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 8ea67fe0b76..3ca83fbac73 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 @@ -70,7 +70,7 @@ class InputService : AccessibilityService() { private val logTag = "input service" private var leftIsDown = false - private val touchPath = Path() + private var touchPath = Path() private var stroke: GestureDescription.StrokeDescription? = null private var lastTouchGestureStartTime = 0L private var mouseX = 0 @@ -278,7 +278,11 @@ class InputService : AccessibilityService() { } private fun startGesture(x: Int, y: Int) { - touchPath.reset() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + touchPath.reset() + } else { + touchPath = Path() + } touchPath.moveTo(x.toFloat(), y.toFloat()) lastTouchGestureStartTime = System.currentTimeMillis() lastX = x @@ -333,19 +337,49 @@ class InputService : AccessibilityService() { @RequiresApi(Build.VERSION_CODES.N) private fun continueGesture(x: Int, y: Int) { - doDispatchGesture(x, y, true) - touchPath.reset() - touchPath.moveTo(x.toFloat(), y.toFloat()) - lastTouchGestureStartTime = System.currentTimeMillis() - lastX = x - lastY = y + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + doDispatchGesture(x, y, true) + touchPath.reset() + touchPath.moveTo(x.toFloat(), y.toFloat()) + lastTouchGestureStartTime = System.currentTimeMillis() + lastX = x + lastY = y + } else { + touchPath.lineTo(x.toFloat(), y.toFloat()) + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun endGestureBelowO(x: Int, y: Int) { + try { + touchPath.lineTo(x.toFloat(), y.toFloat()) + var duration = System.currentTimeMillis() - lastTouchGestureStartTime + if (duration <= 0) { + duration = 1 + } + val stroke = GestureDescription.StrokeDescription( + touchPath, + 0, + duration + ) + val builder = GestureDescription.Builder() + builder.addStroke(stroke) + Log.d(logTag, "end gesture x:$x y:$y time:$duration") + dispatchGesture(builder.build(), null, null) + } catch (e: Exception) { + Log.e(logTag, "endGesture error:$e") + } } @RequiresApi(Build.VERSION_CODES.N) private fun endGesture(x: Int, y: Int) { - doDispatchGesture(x, y, false) - touchPath.reset() - stroke = null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + doDispatchGesture(x, y, false) + touchPath.reset() + stroke = null + } else { + endGestureBelowO(x, y) + } } @RequiresApi(Build.VERSION_CODES.N) 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 5c54c18fb82..a19c2ae9d06 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 @@ -316,7 +316,7 @@ class MainActivity : FlutterActivity() { codecObject.put("mime_type", mime_type) val caps = codec.getCapabilitiesForType(mime_type) if (codec.isEncoder) { - // Encoder‘s max_height and max_width are interchangeable + // Encoder's max_height and max_width are interchangeable if (!caps.videoCapabilities.isSizeSupported(w,h) && !caps.videoCapabilities.isSizeSupported(h,w)) { return@forEach } From 98d99fae645076738a2a015be1ea7ffea99f0d3c Mon Sep 17 00:00:00 2001 From: solokot Date: Sat, 21 Jun 2025 11:29:11 +0300 Subject: [PATCH 361/506] Update ru.rs (#12096) --- 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 168ed36abc4..15e156e9208 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -699,7 +699,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trackpad speed", "Скорость трекпада"), ("Default trackpad speed", "Скорость трекпада по умолчанию"), ("Numeric one-time password", "Цифровой одноразовый пароль"), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("Enable IPv6 P2P connection", "Использовать подключение IPv6 P2P"), + ("Enable UDP hole punching", "Использовать UDP hole punching"), ].iter().cloned().collect(); } From 7822d3d92362ec5a1ed0592968b728f88a917408 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Sat, 21 Jun 2025 10:29:23 +0200 Subject: [PATCH 362/506] Italian language update (#12095) --- 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 fdea76c3617..77addcf64df 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -699,7 +699,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trackpad speed", "Velocità trackpad"), ("Default trackpad speed", "Velocità predefinita trackpad"), ("Numeric one-time password", "Password numerica monouso"), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("Enable IPv6 P2P connection", "Abilita connessione P2P IPv6"), + ("Enable UDP hole punching", "Abilita hole punching UDP"), ].iter().cloned().collect(); } From fa61693ccd9b299160b30f1da77b80b446b7df86 Mon Sep 17 00:00:00 2001 From: Adam Lewicki <160427461+alewicki95@users.noreply.github.com> Date: Sun, 22 Jun 2025 07:48:31 +0200 Subject: [PATCH 363/506] Update pl.rs (#12118) --- src/lang/pl.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 2ea8b4153ba..69939eb9d61 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -694,12 +694,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("download-new-version-failed-tip", "Pobieranie nie powiodło się. Możesz spróbować ponownie lub kliknąć przycisk \"Pobierz\", aby pobrać ze strony programu i uaktualnić ręcznie."), ("Auto update", "Automatyczna aktualizacja"), ("update-failed-check-msi-tip", "Sprawdzenie metody instalacji nie powiodło się. Kliknij przycisk \"Pobierz\", aby pobrać ze strony wydania i uaktualnić ręcznie."), - ("websocket_tip", ""), - ("Use WebSocket", ""), - ("Trackpad speed", ""), - ("Default trackpad speed", ""), - ("Numeric one-time password", ""), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("websocket_tip", "Gdy używasz WebSocket, obsługiwane są tylko połączenia przekaźnikowe."), + ("Use WebSocket", "Użyj WebSocket"), + ("Trackpad speed", "Szybkość gładzika"), + ("Default trackpad speed", "Domyślna szybkość gładzika"), + ("Numeric one-time password", "Jednorazowe hasło cyfrowe"), + ("Enable IPv6 P2P connection", "Włącz połączenie P2P IPv6"), + ("Enable UDP hole punching", "Włącz tworzenie tunelu UDP"), ].iter().cloned().collect(); } From 50b1c02243f5a0d612efd9dcd0c6f9f686561423 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 23 Jun 2025 20:29:24 +0800 Subject: [PATCH 364/506] fix dup msg for relay request --- src/rendezvous_mediator.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index dca1f072135..d5c164800d8 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -36,6 +36,7 @@ type Message = RendezvousMessage; lazy_static::lazy_static! { static ref SOLVING_PK_MISMATCH: Mutex = Default::default(); static ref LAST_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now())); + static ref LAST_RELAY_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now())); } static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false); @@ -396,6 +397,14 @@ impl RendezvousMediator { } async fn handle_request_relay(&self, rr: RequestRelay, server: ServerPtr) -> ResultType<()> { + let addr = AddrMangle::decode(&rr.socket_addr); + let last = *LAST_RELAY_MSG.lock().await; + *LAST_RELAY_MSG.lock().await = (addr, Instant::now()); + // skip duplicate relay request messages + if last.0 == addr && last.1.elapsed().as_millis() < 100 { + return Ok(()); + } + self.create_relay( rr.socket_addr.into(), rr.relay_server, From 4d8bfab86e436966db655c7f5d6b34bb86c4230e Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 23 Jun 2025 23:55:07 +0800 Subject: [PATCH 365/506] fix: sequentially post conn audit (#12152) * fix: sequentially post conn audit Signed-off-by: fufesou * Update connection.rs * refact: simplify loop Signed-off-by: fufesou * Update connection.rs --------- Signed-off-by: fufesou Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- src/server/connection.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 071eaf07e3a..bdf027f935d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -247,6 +247,9 @@ pub struct Connection { multi_ui_session: bool, tx_from_authed: mpsc::UnboundedSender, printer_data: Vec<(Instant, String, Vec)>, + // For post requests that need to be sent sequentially. + // eg. post_conn_audit + tx_post_seq: mpsc::UnboundedSender<(String, Value)>, } impl ConnInner { @@ -321,6 +324,11 @@ impl Connection { let linux_headless_handle = LinuxHeadlessHandle::new(_rx_cm_stream_ready, _tx_desktop_ready); + let (tx_post_seq, rx_post_seq) = mpsc::unbounded_channel(); + tokio::spawn(async move { + Self::post_seq_loop(rx_post_seq).await; + }); + #[cfg(not(any(target_os = "android", target_os = "ios")))] let tx_cloned = tx.clone(); let mut conn = Self { @@ -401,6 +409,7 @@ impl Connection { retina: Retina::default(), tx_from_authed, printer_data: Vec::new(), + tx_post_seq, }; let addr = hbb_common::try_into_v4(addr); if !conn.on_open(addr).await { @@ -960,7 +969,14 @@ impl Connection { } #[cfg(target_os = "linux")] clear_remapped_keycode(); - log::info!("Input thread exited"); + log::debug!("Input thread exited"); + } + + async fn post_seq_loop(mut rx: mpsc::UnboundedReceiver<(String, Value)>) { + while let Some((url, v)) = rx.recv().await { + allow_err!(Self::post_audit_async(url, v).await); + } + log::debug!("post_seq_loop exited"); } async fn try_port_forward_loop( @@ -1117,9 +1133,7 @@ impl Connection { v["uuid"] = json!(crate::encode64(hbb_common::get_uuid())); v["conn_id"] = json!(self.inner.id); v["session_id"] = json!(self.lr.session_id); - tokio::spawn(async move { - allow_err!(Self::post_audit_async(url, v).await); - }); + allow_err!(self.tx_post_seq.send((url, v))); } fn get_files_for_audit(job_type: fs::JobType, mut files: Vec) -> Vec<(String, i64)> { From 2ae7f00cebd3feb6acc0168f3de165bfa50a9a45 Mon Sep 17 00:00:00 2001 From: Mahdi Rahimi <31624047+mahdirahimi1999@users.noreply.github.com> Date: Tue, 24 Jun 2025 08:53:35 +0330 Subject: [PATCH 366/506] Updated Persian translations in fa.rs (#12133) --- src/lang/fa.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 093dc6f667d..2dacb9dd7de 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -699,7 +699,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trackpad speed", "سرعت ترک‌پد"), ("Default trackpad speed", "سرعت پیش‌فرض ترک‌پد"), ("Numeric one-time password", "رمز عبور یک‌بار مصرف عددی"), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("Enable IPv6 P2P connection", "فعال‌سازی اتصال همتا‌به‌همتای IPv6"), + ("Enable UDP hole punching", "فعال‌سازی تکنیک UDP hole punching"), ].iter().cloned().collect(); } From 18ea3a4b59b052e0fee0a3f200b008c90b49bd35 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 24 Jun 2025 21:38:36 +0800 Subject: [PATCH 367/506] Update common.rs --- src/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index c4e43e56920..214d6c1ab8d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1807,7 +1807,7 @@ async fn stun_ipv4_test(stun_server: &str) -> ResultType<(SocketAddr, String)> { static STUNS_V4: [&str; 3] = [ "stun.l.google.com:19302", "stun.cloudflare.com:3478", - "stun.chat.bilibili.com:3478", + "stun.nextcloud.com:3478", ]; static STUNS_V6: [&str; 3] = [ From 79c6da98d2859fb8929a9411e5551f62d149760a Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 24 Jun 2025 21:38:59 +0800 Subject: [PATCH 368/506] Update common.rs (#12159) From 94ae3886c53be8e34b703cdc2f35817b7d90fd5b Mon Sep 17 00:00:00 2001 From: Mahdi Rahimi <31624047+mahdirahimi1999@users.noreply.github.com> Date: Wed, 25 Jun 2025 08:13:35 +0330 Subject: [PATCH 369/506] Update Arabic translation in ar.rs (#12134) --- src/lang/ar.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ar.rs b/src/lang/ar.rs index a337e5f20bd..e03d0e2823b 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -699,7 +699,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trackpad speed", "سرعة لوحة التتبع"), ("Default trackpad speed", "سرعة لوحة التتبع الافتراضية"), ("Numeric one-time password", "كلمة مرور رقمية لمرة واحدة"), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("Enable IPv6 P2P connection", "تمكين اتصال نظير إلى نظير عبر IPv6"), + ("Enable UDP hole punching", "تمكين تقنية حفر الثغرات عبر UDP"), ].iter().cloned().collect(); } From 7b7c93b78d6155f51dce6095ce9fc3445d9bc4ce Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 26 Jun 2025 09:29:41 +0800 Subject: [PATCH 370/506] fix record directory of custom client (#12171) * For custom client, the incoming record directory of installing Windows app and the Android record directory still use RustDesk, it works, but replace 'RustDesk' with the custom client's name. Signed-off-by: 21pages --- src/ui_interface.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index d7edf4efb7c..e17e82fcee6 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -3,10 +3,7 @@ use hbb_common::password_security; use hbb_common::{ allow_err, bytes::Bytes, - config::{ - self, keys::*, Config, LocalConfig, PeerConfig, CONNECT_TIMEOUT, - RENDEZVOUS_PORT, - }, + config::{self, keys::*, Config, LocalConfig, PeerConfig, CONNECT_TIMEOUT, RENDEZVOUS_PORT}, directories_next, futures::future::join_all, log, @@ -863,7 +860,7 @@ pub fn video_save_directory(root: bool) -> String { { let drive = std::env::var("SystemDrive").unwrap_or("C:".to_owned()); let dir = - std::path::PathBuf::from(format!("{drive}\\ProgramData\\RustDesk\\recording",)); + std::path::PathBuf::from(format!("{drive}\\ProgramData\\{appname}\\recording",)); return dir.to_string_lossy().to_string(); } } @@ -878,7 +875,7 @@ pub fn video_save_directory(root: bool) -> String { #[cfg(any(target_os = "android", target_os = "ios"))] if let Ok(home) = config::APP_HOME_DIR.read() { let mut path = home.to_owned(); - path.push_str("/RustDesk/ScreenRecord"); + path.push_str(format!("/{appname}/ScreenRecord").as_str()); let dir = try_create(&std::path::Path::new(&path)); if !dir.is_empty() { return dir; @@ -1297,7 +1294,11 @@ pub async fn change_id_shared(id: String, old_id: String) -> String { pub async fn change_id_shared_(id: String, old_id: String) -> &'static str { if !hbb_common::is_valid_custom_id(&id) { - log::debug!("debugging invalid id: \"{id}\", len: {}, base64: \"{}\"", id.len(), crate::encode64(&id)); + log::debug!( + "debugging invalid id: \"{id}\", len: {}, base64: \"{}\"", + id.len(), + crate::encode64(&id) + ); let bom = id.trim_start_matches('\u{FEFF}'); log::debug!("bom: {}", hbb_common::is_valid_custom_id(&bom)); return INVALID_FORMAT; From bb6e080c1c69695054a8bc99fe976ec4c12e3446 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 26 Jun 2025 09:49:22 +0800 Subject: [PATCH 371/506] fix: linux workaround cmd path (#12172) Signed-off-by: fufesou --- libs/hbb_common | 2 +- src/platform/gtk_sudo.rs | 6 ++++-- src/platform/linux.rs | 13 ++++++++----- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/libs/hbb_common b/libs/hbb_common index 92ca2ca8be6..3454fe8c60d 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 92ca2ca8be6c54d3977e35292d378dfbe90908fc +Subproject commit 3454fe8c60d32ee5033d04fc72466eebb5873880 diff --git a/src/platform/gtk_sudo.rs b/src/platform/gtk_sudo.rs index 9aeea1e2b01..fca6403f657 100644 --- a/src/platform/gtk_sudo.rs +++ b/src/platform/gtk_sudo.rs @@ -5,7 +5,9 @@ use crate::lang::translate; use gtk::{glib, prelude::*}; use hbb_common::{ anyhow::{bail, Error}, - log, ResultType, + log, + platform::linux::CMD_SH, + ResultType, }; use nix::{ libc::{fcntl, kill}, @@ -468,7 +470,7 @@ fn child(su_user: Option, args: Vec) -> ResultType<()> { if su_user.is_some() { params.push("-S".to_string()); } - params.push("/bin/sh".to_string()); + params.push(CMD_SH.to_string()); params.push("-c".to_string()); let command = args diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 18a8ab1722a..7d8b781a71d 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -10,6 +10,7 @@ use hbb_common::{ libc::{c_char, c_int, c_long, c_void}, log, message_proto::{DisplayInfo, Resolution}, + platform::linux::{CMD_PS, CMD_SH}, regex::{Captures, Regex}, }; use std::{ @@ -320,7 +321,7 @@ fn set_x11_env(desktop: &Desktop) { #[inline] fn stop_rustdesk_servers() { let _ = run_cmds(&format!( - r##"ps -ef | grep -E '{} +--server' | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##, + r##"ps -ef | grep -E '{} +--server' | awk '{{print $2}}' | xargs -r kill -9"##, crate::get_app_name().to_lowercase(), )); } @@ -328,11 +329,11 @@ fn stop_rustdesk_servers() { #[inline] fn stop_subprocess() { let _ = run_cmds(&format!( - r##"ps -ef | grep '/etc/{}/xorg.conf' | grep -v grep | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##, + r##"ps -ef | grep '/etc/{}/xorg.conf' | grep -v grep | awk '{{print $2}}' | xargs -r kill -9"##, crate::get_app_name().to_lowercase(), )); let _ = run_cmds(&format!( - r##"ps -ef | grep -E '{} +--cm-no-ui' | grep -v grep | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##, + r##"ps -ef | grep -E '{} +--cm-no-ui' | grep -v grep | awk '{{print $2}}' | xargs -r kill -9"##, crate::get_app_name().to_lowercase(), )); } @@ -517,7 +518,8 @@ pub fn get_active_userid() -> String { } fn get_cm() -> bool { - if let Ok(output) = Command::new("ps").args(vec!["aux"]).output() { + // We use `CMD_PS` instead of `ps` to suppress some audit messages on some systems. + if let Ok(output) = Command::new(CMD_PS.as_str()).args(vec!["aux"]).output() { for line in String::from_utf8_lossy(&output.stdout).lines() { if line.contains(&format!( "{} --cm", @@ -1355,7 +1357,8 @@ pub fn run_me_with(secs: u32) { .unwrap_or("".into()) .to_string_lossy() .to_string(); - std::process::Command::new("sh") + // We use `CMD_SH` instead of `sh` to suppress some audit messages on some systems. + std::process::Command::new(CMD_SH.as_str()) .arg("-c") .arg(&format!("sleep {secs}; {exe}")) .spawn() From 58fd2d3ccd0c3149e3f3ebe5afed423b8d7525f9 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:47:01 +0800 Subject: [PATCH 372/506] fix: linux, get_env, break loop (#12174) Signed-off-by: fufesou --- src/platform/linux.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 7d8b781a71d..9fa69fa63ae 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1047,7 +1047,7 @@ mod desktop { fn get_display_xauth_xwayland(&mut self) { let tray = format!("{} +--tray", crate::get_app_name().to_lowercase()); - for _ in 0..5 { + for _ in 1..=10 { let display_proc = vec![ XWAYLAND, IBUS_DAEMON, @@ -1060,7 +1060,7 @@ mod desktop { self.xauth = get_env("XAUTHORITY", &self.uid, proc); self.wl_display = get_env("WAYLAND_DISPLAY", &self.uid, proc); if !self.display.is_empty() && !self.xauth.is_empty() { - break; + return; } } sleep_millis(300); @@ -1068,7 +1068,7 @@ mod desktop { } fn get_display_x11(&mut self) { - for _ in 0..10 { + for _ in 1..=10 { let display_proc = vec![ XWAYLAND, IBUS_DAEMON, @@ -1083,6 +1083,9 @@ mod desktop { break; } } + if !self.display.is_empty() { + break; + } sleep_millis(300); } @@ -1152,7 +1155,7 @@ mod desktop { fn get_xauth_x11(&mut self) { // try by direct access to window manager process by name let tray = format!("{} +--tray", crate::get_app_name().to_lowercase()); - for _ in 0..10 { + for _ in 1..=10 { let display_proc = vec![ XWAYLAND, IBUS_DAEMON, @@ -1168,6 +1171,9 @@ mod desktop { break; } } + if !self.xauth.is_empty() { + break; + } sleep_millis(300); } From fd4e0146e1002554ef7284415fbfe131408f13c8 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:54:16 +0800 Subject: [PATCH 373/506] fix: replace sh with CMD_SH (#12173) Signed-off-by: fufesou --- libs/scrap/src/wayland/pipewire.rs | 3 ++- src/clipboard.rs | 3 ++- src/server/wayland.rs | 7 +++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 2f1e2a85267..9b2c5a6e051 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -24,6 +24,7 @@ use super::capturable::{Capturable, Recorder}; use super::remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop as remote_desktop_portal; use super::request_portal::OrgFreedesktopPortalRequestResponse; use super::screencast_portal::OrgFreedesktopPortalScreenCast as screencast_portal; +use hbb_common::platform::linux::CMD_SH; use lazy_static::lazy_static; lazy_static! { @@ -880,7 +881,7 @@ pub fn get_capturables() -> Result, Box> { // `remote_desktop_portal` does not support restore_token and persist_mode. fn is_server_running() -> bool { let app_name = config::APP_NAME.read().unwrap().clone().to_lowercase(); - let output = match Command::new("sh") + let output = match Command::new(CMD_SH.as_str()) .arg("-c") .arg(&format!("ps aux | grep {}", app_name)) .output() diff --git a/src/clipboard.rs b/src/clipboard.rs index 3ab95e41e75..db8cb4cfec2 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -427,7 +427,8 @@ impl ClipboardContext { // It's not correct in the server process. #[cfg(target_os = "linux")] let is_kde_x11 = { - let is_kde = std::process::Command::new("sh") + use hbb_common::platform::linux::CMD_SH; + let is_kde = std::process::Command::new(CMD_SH.as_str()) .arg("-c") .arg("ps -e | grep -E kded[0-9]+ | grep -v grep") .stdout(std::process::Stdio::piped()) diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 6c64e0d9fdb..42c6132777e 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -1,5 +1,8 @@ use super::*; -use hbb_common::{allow_err, platform::linux::DISTRO}; +use hbb_common::{ + allow_err, + platform::linux::{CMD_SH, DISTRO}, +}; use scrap::{is_cursor_embedded, set_map_err, Capturer, Display, Frame, TraitCapturer}; use std::io; use std::process::{Command, Output}; @@ -109,7 +112,7 @@ pub(super) fn is_inited() -> Option { fn get_max_desktop_resolution() -> Option { // works with Xwayland - let output: Output = Command::new("sh") + let output: Output = Command::new(CMD_SH.as_str()) .arg("-c") .arg("xrandr | awk '/current/ { print $8,$9,$10 }'") .output() From 9060f9ec8a6aac2acc01cbbc4fe47b627e813d0a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:27:22 +0800 Subject: [PATCH 374/506] fix: linux tray, defunct process (#12177) Signed-off-by: fufesou --- src/tray.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/tray.rs b/src/tray.rs index 98ce450e1c7..f36da2cec41 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -21,6 +21,10 @@ pub fn start_tray() { return; } } + + #[cfg(target_os = "linux")] + crate::server::check_zombie(); + allow_err!(make_tray()); } @@ -99,9 +103,11 @@ fn make_tray() -> hbb_common::ResultType<()> { } #[cfg(target_os = "linux")] { - // Do not use "xdg-open", it won't read config + // Do not use "xdg-open", it won't read the config. if crate::dbus::invoke_new_connection(crate::get_uri_prefix()).is_err() { - crate::run_me::<&str>(vec![]).ok(); + if let Ok(task) = crate::run_me::<&str>(vec![]) { + crate::server::CHILD_PROCESS.lock().unwrap().push(task); + } } } }; From 884373794a5900e26dbedbde8fa7d1a904fbe8ac Mon Sep 17 00:00:00 2001 From: Andrzej Rudnik Date: Fri, 27 Jun 2025 12:49:25 +0200 Subject: [PATCH 375/506] Update pl.rs (#12162) --- src/lang/pl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 69939eb9d61..5b79b679653 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -689,7 +689,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Copy to clipboard", "Kopiuj do schowka"), ("Enable remote printer", "Włącz zdalne drukowanie"), ("Downloading {}", "Pobieranie {}"), - ("{} Update", "Aktualizacji {}"), + ("{} Update", "Aktualizacja {}"), ("{}-to-update-tip", "{} zostanie teraz zamknięty i zostanie zainstalowana nowa wersja."), ("download-new-version-failed-tip", "Pobieranie nie powiodło się. Możesz spróbować ponownie lub kliknąć przycisk \"Pobierz\", aby pobrać ze strony programu i uaktualnić ręcznie."), ("Auto update", "Automatyczna aktualizacja"), From d21a1023d27682cd2616c65dd0985efa378e2fb1 Mon Sep 17 00:00:00 2001 From: Melroy dsilva <72267429+melroy12@users.noreply.github.com> Date: Sat, 28 Jun 2025 16:26:00 +0900 Subject: [PATCH 376/506] docs: improve grammar and clarity in READM (#12155) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index aa83d188e47..f29be76947a 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). +Yet another remote desktop solution, written in Rust. Works out of the box with no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) @@ -46,7 +46,7 @@ Please download Sciter dynamic library yourself. [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) -## Raw steps to build +## Raw Steps to build - Prepare your Rust development env and C++ build env @@ -59,7 +59,7 @@ Please download Sciter dynamic library yourself. ## [Build](https://rustdesk.com/docs/en/dev/build/) -## How to build on Linux +## How to Build on Linux ### Ubuntu 18 (Debian 10) @@ -154,7 +154,7 @@ Or, if you're running a release executable: target/release/rustdesk ``` -Please ensure that you are running these commands from the root of the RustDesk repository, otherwise the application might not be able to find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host. +Please ensure that you run these commands from the root of the RustDesk repository, or the application may not find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host. ## File Structure From e0f5fa39f368633a0ef292ac70c591e0e08c31c8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 29 Jun 2025 14:09:59 +0800 Subject: [PATCH 377/506] terminal of hbb common --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index 3454fe8c60d..117ea7c3414 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 3454fe8c60d32ee5033d04fc72466eebb5873880 +Subproject commit 117ea7c3414ccb43dc9a7b608616d339bdbe17d1 From ee5cdc3155ae4f0eae7a91ed46d81a05dc527859 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Mon, 30 Jun 2025 08:59:21 +0200 Subject: [PATCH 378/506] Update nl.rs (#12194) --- 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 e09f8855172..b7b50cf9084 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -699,7 +699,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trackpad speed", "Snelheid Trackpad"), ("Default trackpad speed", "Standaardsnelheid Trackpad"), ("Numeric one-time password", "Eenmalig numeriek wachtwoord"), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("Enable IPv6 P2P connection", "IPv6 P2P-verbinding inschakelen"), + ("Enable UDP hole punching", "UDP-hole punching inschakelen"), ].iter().cloned().collect(); } From 5faf0ad3cfef1b7237e6cbca53038d76ffdc281d Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:12:55 +0800 Subject: [PATCH 379/506] terminal works basically. (#12189) * terminal works basically. todo: - persistent - sessions restore - web - mobile * missed terminal persistent option change * android sdk 34 -> 35 * +#![cfg_attr(lt_1_77, feature(c_str_literals))] * fixing ci * fix ci * fix ci for android * try "Fix Android SDK Platform 35" * fix android 34 * revert flutter_plugin_android_lifecycle to 2.0.17 which used in rustdesk 1.4.0 * refactor, but break something of desktop terminal (new tab showing loading) * fix connecting... --- .github/workflows/bridge.yml | 4 +- .github/workflows/flutter-build.yml | 16 +- .github/workflows/playground.yml | 4 +- Cargo.lock | 142 +- Cargo.toml | 2 +- .../com/carriez/flutter_hbb/MainService.kt | 4 +- flutter/lib/common.dart | 97 +- flutter/lib/common/widgets/peer_card.dart | 20 +- .../lib/common/widgets/setting_widgets.dart | 3 +- flutter/lib/common/widgets/toolbar.dart | 18 +- flutter/lib/consts.dart | 5 +- .../lib/desktop/pages/connection_page.dart | 51 +- .../lib/desktop/pages/desktop_home_page.dart | 1 + .../desktop/pages/desktop_setting_page.dart | 2 + flutter/lib/desktop/pages/server_page.dart | 32 +- .../pages/terminal_connection_manager.dart | 98 ++ flutter/lib/desktop/pages/terminal_page.dart | 121 ++ .../lib/desktop/pages/terminal_tab_page.dart | 384 +++++ .../lib/desktop/pages/view_camera_page.dart | 2 - .../screen/desktop_terminal_screen.dart | 27 + .../lib/desktop/widgets/tabbar_widget.dart | 1 + flutter/lib/main.dart | 15 + .../lib/mobile/pages/file_manager_page.dart | 6 +- flutter/lib/mobile/pages/home_page.dart | 13 +- flutter/lib/mobile/pages/remote_page.dart | 4 +- flutter/lib/mobile/pages/server_page.dart | 6 +- flutter/lib/mobile/pages/settings_page.dart | 2 +- flutter/lib/mobile/pages/terminal_page.dart | 106 ++ .../lib/mobile/pages/view_camera_page.dart | 4 +- flutter/lib/models/model.dart | 63 +- flutter/lib/models/server_model.dart | 16 +- flutter/lib/models/terminal_model.dart | 269 +++ flutter/lib/utils/multi_window_manager.dart | 34 + flutter/lib/web/bridge.dart | 62 +- flutter/macos/Podfile.lock | 52 +- flutter/pubspec.lock | 459 ++--- flutter/pubspec.yaml | 6 +- flutter/web/v1/.gitignore | 9 - flutter/web/v1/README.md | 1 - flutter/web/v1/index.html | 183 -- flutter/web/v1/js/.gitattributes | 1 - flutter/web/v1/js/.gitignore | 9 - flutter/web/v1/js/.yarnrc.yml | 1 - flutter/web/v1/js/gen_js_from_hbb.py | 77 - flutter/web/v1/js/index.html | 15 - flutter/web/v1/js/package.json | 22 - flutter/web/v1/js/src/codec.js | 43 - flutter/web/v1/js/src/common.ts | 77 - flutter/web/v1/js/src/connection.ts | 773 --------- flutter/web/v1/js/src/globals.js | 383 ----- flutter/web/v1/js/src/main.ts | 2 - flutter/web/v1/js/src/style.css | 8 - flutter/web/v1/js/src/ui.js | 108 -- flutter/web/v1/js/src/vite-env.d.ts | 1 - flutter/web/v1/js/src/websock.ts | 183 -- flutter/web/v1/js/ts_proto.py | 20 - flutter/web/v1/js/tsconfig.json | 24 - flutter/web/v1/js/vite.config.js | 14 - flutter/web/v1/js/yarn.lock | 1494 ----------------- flutter/web/v1/libs/firebase-analytics.js | 2 - flutter/web/v1/libs/firebase-app.js | 2 - flutter/web/v1/manifest.json | 35 - flutter/web/v1/yarn.lock | 4 - flutter/web/v1/yuv.js | 73 - flutter/web/v1/yuv.wasm | Bin 8238 -> 0 bytes flutter/web/v2/README.md | 1 - libs/hbb_common | 2 +- src/client.rs | 48 +- src/client/io_loop.rs | 73 +- src/flutter.rs | 57 + src/flutter_ffi.rs | 31 + src/ipc.rs | 1 + src/lang/ar.rs | 14 +- src/lang/be.rs | 14 +- src/lang/bg.rs | 14 +- src/lang/ca.rs | 14 +- src/lang/cn.rs | 14 +- src/lang/cs.rs | 14 +- src/lang/da.rs | 14 +- src/lang/de.rs | 14 +- src/lang/el.rs | 14 +- src/lang/en.rs | 3 - src/lang/eo.rs | 14 +- src/lang/es.rs | 14 +- src/lang/et.rs | 14 +- src/lang/eu.rs | 14 +- src/lang/fa.rs | 14 +- src/lang/fr.rs | 14 +- src/lang/ge.rs | 14 +- src/lang/he.rs | 14 +- src/lang/hr.rs | 14 +- src/lang/hu.rs | 14 +- src/lang/id.rs | 14 +- src/lang/it.rs | 14 +- src/lang/ja.rs | 14 +- src/lang/ko.rs | 14 +- src/lang/kz.rs | 14 +- src/lang/lt.rs | 14 +- src/lang/lv.rs | 14 +- src/lang/nb.rs | 14 +- src/lang/nl.rs | 14 +- src/lang/pl.rs | 14 +- src/lang/pt_PT.rs | 14 +- src/lang/ptbr.rs | 14 +- src/lang/ro.rs | 14 +- src/lang/ru.rs | 14 +- src/lang/sc.rs | 14 +- src/lang/sk.rs | 14 +- src/lang/sl.rs | 14 +- src/lang/sq.rs | 14 +- src/lang/sr.rs | 14 +- src/lang/sv.rs | 14 +- src/lang/ta.rs | 14 +- src/lang/template.rs | 14 +- src/lang/th.rs | 14 +- src/lang/tr.rs | 14 +- src/lang/tw.rs | 14 +- src/lang/uk.rs | 14 +- src/lang/vi.rs | 14 +- src/lib.rs | 2 +- src/server.rs | 3 + src/server/connection.rs | 126 +- src/server/terminal_service.rs | 968 +++++++++++ src/ui/cm.rs | 1 + src/ui/cm.tis | 17 +- src/ui/index.tis | 4 +- src/ui/remote.rs | 13 +- src/ui_cm_interface.rs | 11 +- src/ui_session_interface.rs | 70 +- terminal.md | 521 ++++++ 130 files changed, 4064 insertions(+), 4247 deletions(-) create mode 100644 flutter/lib/desktop/pages/terminal_connection_manager.dart create mode 100644 flutter/lib/desktop/pages/terminal_page.dart create mode 100644 flutter/lib/desktop/pages/terminal_tab_page.dart create mode 100644 flutter/lib/desktop/screen/desktop_terminal_screen.dart create mode 100644 flutter/lib/mobile/pages/terminal_page.dart create mode 100644 flutter/lib/models/terminal_model.dart delete mode 100644 flutter/web/v1/.gitignore delete mode 100644 flutter/web/v1/README.md delete mode 100644 flutter/web/v1/index.html delete mode 100644 flutter/web/v1/js/.gitattributes delete mode 100644 flutter/web/v1/js/.gitignore delete mode 100644 flutter/web/v1/js/.yarnrc.yml delete mode 100755 flutter/web/v1/js/gen_js_from_hbb.py delete mode 100644 flutter/web/v1/js/index.html delete mode 100644 flutter/web/v1/js/package.json delete mode 100644 flutter/web/v1/js/src/codec.js delete mode 100644 flutter/web/v1/js/src/common.ts delete mode 100644 flutter/web/v1/js/src/connection.ts delete mode 100644 flutter/web/v1/js/src/globals.js delete mode 100644 flutter/web/v1/js/src/main.ts delete mode 100644 flutter/web/v1/js/src/style.css delete mode 100644 flutter/web/v1/js/src/ui.js delete mode 100644 flutter/web/v1/js/src/vite-env.d.ts delete mode 100644 flutter/web/v1/js/src/websock.ts delete mode 100755 flutter/web/v1/js/ts_proto.py delete mode 100644 flutter/web/v1/js/tsconfig.json delete mode 100644 flutter/web/v1/js/vite.config.js delete mode 100644 flutter/web/v1/js/yarn.lock delete mode 100644 flutter/web/v1/libs/firebase-analytics.js delete mode 100644 flutter/web/v1/libs/firebase-app.js delete mode 100644 flutter/web/v1/manifest.json delete mode 100644 flutter/web/v1/yarn.lock delete mode 100644 flutter/web/v1/yuv.js delete mode 100644 flutter/web/v1/yuv.wasm delete mode 100644 flutter/web/v2/README.md create mode 100644 src/server/terminal_service.rs create mode 100644 terminal.md diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index d91dc25fb3f..1913132e2ce 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -40,9 +40,9 @@ jobs: gcc \ git \ g++ \ - libclang-11-dev \ + libclang-dev \ libgtk-3-dev \ - llvm-11-dev \ + llvm-dev \ nasm \ ninja-build \ pkg-config \ diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 27b6118b90e..36bbe790203 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -929,21 +929,21 @@ jobs: - { arch: aarch64, target: aarch64-linux-android, - os: ubuntu-22.04, + os: ubuntu-24.04, reltype: release, suffix: "", } - { arch: armv7, target: armv7-linux-androideabi, - os: ubuntu-22.04, + os: ubuntu-24.04, reltype: release, suffix: "", } - { arch: x86_64, target: x86_64-linux-android, - os: ubuntu-22.04, + os: ubuntu-24.04, reltype: release, suffix: "", } @@ -980,7 +980,7 @@ jobs: libayatana-appindicator3-dev \ libasound2-dev \ libc6-dev \ - libclang-11-dev \ + libclang-dev \ libunwind-dev \ libgstreamer1.0-dev \ libgstreamer-plugins-base1.0-dev \ @@ -993,7 +993,7 @@ jobs: libxcb-xfixes0-dev \ libxdo-dev \ libxfixes-dev \ - llvm-11-dev \ + llvm-dev \ nasm \ ninja-build \ openjdk-17-jdk-headless \ @@ -1212,7 +1212,7 @@ jobs: needs: [build-rustdesk-android] name: build rustdesk android universal apk if: ${{ inputs.upload-artifact }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: reltype: release x86_target: "" # can be ",android-x86" @@ -1250,7 +1250,7 @@ jobs: libayatana-appindicator3-dev \ libasound2-dev \ libc6-dev \ - libclang-11-dev \ + libclang-dev \ libunwind-dev \ libgstreamer1.0-dev \ libgstreamer-plugins-base1.0-dev \ @@ -1263,7 +1263,7 @@ jobs: libxcb-xfixes0-dev \ libxdo-dev \ libxfixes-dev \ - llvm-11-dev \ + llvm-dev \ nasm \ ninja-build \ openjdk-17-jdk-headless \ diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 962df73f1cb..48e5c4df0dd 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -266,7 +266,7 @@ jobs: libayatana-appindicator3-dev\ libasound2-dev \ libc6-dev \ - libclang-11-dev \ + libclang-dev \ libunwind-dev \ libgstreamer1.0-dev \ libgstreamer-plugins-base1.0-dev \ @@ -280,7 +280,7 @@ jobs: libxcb-xfixes0-dev \ libxdo-dev \ libxfixes-dev \ - llvm-11-dev \ + llvm-dev \ nasm \ yasm \ ninja-build \ diff --git a/Cargo.lock b/Cargo.lock index 440e02422d2..20e1b34ab7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2216,6 +2216,17 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.61", + "winapi 0.3.9", +] + [[package]] name = "filetime" version = "0.2.23" @@ -3511,6 +3522,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ioctl-rs" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -3948,7 +3968,7 @@ source = "git+https://github.com/rustdesk-org/machine-uid#381ff579c1dc3a6c54db9d dependencies = [ "bindgen 0.59.2", "cc", - "winreg", + "winreg 0.11.0", ] [[package]] @@ -4286,6 +4306,20 @@ dependencies = [ "memoffset 0.6.5", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg 1.3.0", + "bitflags 1.3.2", + "cfg-if 1.0.0", + "libc", + "memoffset 0.6.5", + "pin-utils", +] + [[package]] name = "nix" version = "0.26.4" @@ -5196,6 +5230,27 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "portable-pty" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806ee80c2a03dbe1a9fb9534f8d19e4c0546b790cde8fd1fea9d6390644cb0be" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix 0.25.1", + "serial", + "shared_library", + "shell-words", + "winapi 0.3.9", + "winreg 0.10.1", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -5391,7 +5446,7 @@ dependencies = [ "cfg-if 0.1.10", "rpassword 2.1.0", "tempfile", - "termios", + "termios 0.3.3", "winapi 0.3.9", ] @@ -6067,6 +6122,7 @@ dependencies = [ "pam", "parity-tokio-ipc", "percent-encoding", + "portable-pty", "qrcode-generator", "rdev", "remote_printer", @@ -6092,7 +6148,7 @@ dependencies = [ "system_shutdown", "tao", "tauri-winrt-notification", - "termios", + "termios 0.3.3", "totp-rs", "tray-icon", "url", @@ -6103,7 +6159,7 @@ dependencies = [ "winapi 0.3.9", "windows 0.61.1", "windows-service", - "winreg", + "winreg 0.11.0", "winres", "wol-rs", "x11-clipboard 0.8.1", @@ -6466,6 +6522,48 @@ dependencies = [ "serde 1.0.203", ] +[[package]] +name = "serial" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86" +dependencies = [ + "serial-core", + "serial-unix", + "serial-windows", +] + +[[package]] +name = "serial-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581" +dependencies = [ + "libc", +] + +[[package]] +name = "serial-unix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7" +dependencies = [ + "ioctl-rs", + "libc", + "serial-core", + "termios 0.2.2", +] + +[[package]] +name = "serial-windows" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162" +dependencies = [ + "libc", + "serial-core", +] + [[package]] name = "sha1" version = "0.10.6" @@ -6510,6 +6608,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + [[package]] name = "shared_memory" version = "0.12.4" @@ -6523,6 +6631,12 @@ dependencies = [ "win-sys", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -6957,6 +7071,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termios" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" +dependencies = [ + "libc", +] + [[package]] name = "termios" version = "0.3.3" @@ -7779,7 +7902,7 @@ dependencies = [ "rust-ini", "thiserror 1.0.61", "winapi 0.3.9", - "winreg", + "winreg 0.11.0", ] [[package]] @@ -8758,6 +8881,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "winreg" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index 7108e923b62..06bfcaeb303 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,6 @@ shutdown_hooks = "0.1" totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } stunclient = "0.4" kcp-sys= { git = "https://github.com/rustdesk-org/kcp-sys"} - [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" } @@ -99,6 +98,7 @@ ctrlc = "3.2" # arboard = { version = "3.4", features = ["wayland-data-control"] } arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] } clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" } +portable-pty = "0.8.1" # higher version not work on rustc 1.75 system_shutdown = "4.0" qrcode-generator = "4.1" 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 c475dd3b019..7bb16a00ad6 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 @@ -122,9 +122,9 @@ class MainService : Service() { val authorized = jsonObject["authorized"] as Boolean val isFileTransfer = jsonObject["is_file_transfer"] as Boolean val type = if (isFileTransfer) { - translate("File Connection") + translate("Transfer file") } else { - translate("Screen Connection") + translate("Share screen") } if (authorized) { if (!isFileTransfer && !isStart) { diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 5b58639bab0..0fc8aa6c0de 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 'mobile/pages/view_camera_page.dart'; +import 'mobile/pages/terminal_page.dart'; import 'desktop/pages/remote_page.dart' as desktop_remote; import 'desktop/pages/file_manager_page.dart' as desktop_file_manager; import 'desktop/pages/view_camera_page.dart' as desktop_view_camera; @@ -99,6 +100,7 @@ enum DesktopType { remote, fileTransfer, viewCamera, + terminal, cm, portForward, } @@ -1571,7 +1573,9 @@ bool option2bool(String option, String value) { String bool2option(String option, bool b) { String res; - if (option.startsWith('enable-') && option != kOptionEnableUdpPunch && option != kOptionEnableIpv6Punch) { + if (option.startsWith('enable-') && + option != kOptionEnableUdpPunch && + option != kOptionEnableIpv6Punch) { res = b ? defaultOptionYes : 'N'; } else if (option.startsWith('allow-') || option == kOptionStopService || @@ -2117,6 +2121,7 @@ enum UriLinkType { viewCamera, portForward, rdp, + terminal, } // uri link handler @@ -2181,6 +2186,11 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { id = args[i + 1]; i++; break; + case '--terminal': + type = UriLinkType.terminal; + id = args[i + 1]; + i++; + break; case '--password': password = args[i + 1]; i++; @@ -2230,6 +2240,12 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { password: password, forceRelay: forceRelay); }); break; + case UriLinkType.terminal: + Future.delayed(Duration.zero, () { + rustDeskWinManager.newTerminal(id!, + password: password, forceRelay: forceRelay); + }); + break; } return true; @@ -2247,7 +2263,8 @@ List? urlLinkToCmdArgs(Uri uri) { "file-transfer", "view-camera", "port-forward", - "rdp" + "rdp", + "terminal" ]; if (uri.authority.isEmpty && uri.path.split('').every((char) => char == '/')) { @@ -2276,21 +2293,10 @@ List? urlLinkToCmdArgs(Uri uri) { } } } else if (options.contains(uri.authority)) { - final optionIndex = options.indexOf(uri.authority); command = '--${uri.authority}'; if (uri.path.length > 1) { id = uri.path.substring(1); } - if (isMobile && id != null) { - if (optionIndex == 0 || optionIndex == 1) { - connect(Get.context!, id); - } else if (optionIndex == 2) { - connect(Get.context!, id, isFileTransfer: true); - } else if (optionIndex == 3) { - connect(Get.context!, id, isViewCamera: true); - } - return null; - } } else if (uri.authority.length > 2 && (uri.path.length <= 1 || (uri.path == '/r' || uri.path.startsWith('/r@')))) { @@ -2314,13 +2320,25 @@ List? urlLinkToCmdArgs(Uri uri) { } } - if (isMobile) { - if (id != null) { - final forceRelay = queryParameters["relay"] != null; + if (isMobile && id != null) { + final forceRelay = queryParameters["relay"] != null; + final password = queryParameters["password"]; + + // Determine connection type based on command + if (command == '--file-transfer') { connect(Get.context!, id, - forceRelay: forceRelay, password: queryParameters["password"]); - return null; + isFileTransfer: true, forceRelay: forceRelay, password: password); + } else if (command == '--view-camera') { + connect(Get.context!, id, + isViewCamera: true, forceRelay: forceRelay, password: password); + } else if (command == '--terminal') { + connect(Get.context!, id, + isTerminal: true, forceRelay: forceRelay, password: password); + } else { + // Default to remote desktop for '--connect', '--play', or direct connection + connect(Get.context!, id, forceRelay: forceRelay, password: password); } + return null; } List args = List.empty(growable: true); @@ -2342,6 +2360,7 @@ List? urlLinkToCmdArgs(Uri uri) { connectMainDesktop(String id, {required bool isFileTransfer, required bool isViewCamera, + required bool isTerminal, required bool isTcpTunneling, required bool isRDP, bool? forceRelay, @@ -2366,6 +2385,12 @@ connectMainDesktop(String id, isSharedPassword: isSharedPassword, connToken: connToken, forceRelay: forceRelay); + } else if (isTerminal) { + await rustDeskWinManager.newTerminal(id, + password: password, + isSharedPassword: isSharedPassword, + connToken: connToken, + forceRelay: forceRelay); } else { await rustDeskWinManager.newRemoteDesktop(id, password: password, @@ -2382,6 +2407,7 @@ connectMainDesktop(String id, connect(BuildContext context, String id, {bool isFileTransfer = false, bool isViewCamera = false, + bool isTerminal = false, bool isTcpTunneling = false, bool isRDP = false, bool forceRelay = false, @@ -2404,7 +2430,7 @@ connect(BuildContext context, String id, id = id.replaceAll(' ', ''); final oldId = id; id = await bind.mainHandleRelayId(id: id); - final forceRelay2 = id != oldId || forceRelay; + forceRelay = id != oldId || forceRelay; assert(!(isFileTransfer && isTcpTunneling && isRDP), "more than one connect type"); @@ -2414,17 +2440,19 @@ connect(BuildContext context, String id, id, isFileTransfer: isFileTransfer, isViewCamera: isViewCamera, + isTerminal: isTerminal, isTcpTunneling: isTcpTunneling, isRDP: isRDP, password: password, isSharedPassword: isSharedPassword, - forceRelay: forceRelay2, + forceRelay: forceRelay, ); } else { await rustDeskWinManager.call(WindowType.Main, kWindowConnect, { 'id': id, 'isFileTransfer': isFileTransfer, 'isViewCamera': isViewCamera, + 'isTerminal': isTerminal, 'isTcpTunneling': isTcpTunneling, 'isRDP': isRDP, 'password': password, @@ -2458,7 +2486,10 @@ connect(BuildContext context, String id, context, MaterialPageRoute( builder: (BuildContext context) => FileManagerPage( - id: id, password: password, isSharedPassword: isSharedPassword), + id: id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay), ), ); } @@ -2473,7 +2504,6 @@ connect(BuildContext context, String id, id: id, toolbarState: ToolbarState(), password: password, - forceRelay: forceRelay, isSharedPassword: isSharedPassword, ), ), @@ -2483,10 +2513,25 @@ connect(BuildContext context, String id, context, MaterialPageRoute( builder: (BuildContext context) => ViewCameraPage( - id: id, password: password, isSharedPassword: isSharedPassword), + id: id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay), ), ); } + } else if (isTerminal) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => TerminalPage( + id: id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay, + ), + ), + ); } else { if (isWeb) { Navigator.push( @@ -2497,7 +2542,6 @@ connect(BuildContext context, String id, id: id, toolbarState: ToolbarState(), password: password, - forceRelay: forceRelay, isSharedPassword: isSharedPassword, ), ), @@ -2507,7 +2551,10 @@ connect(BuildContext context, String id, context, MaterialPageRoute( builder: (BuildContext context) => RemotePage( - id: id, password: password, isSharedPassword: isSharedPassword), + id: id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay), ), ); } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 8c4b019b266..d664f3d801a 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -491,6 +491,7 @@ abstract class BasePeerCard extends StatelessWidget { bool isViewCamera = false, bool isTcpTunneling = false, bool isRDP = false, + bool isTerminal = false, }) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( @@ -506,6 +507,7 @@ abstract class BasePeerCard extends StatelessWidget { isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, isRDP: isRDP, + isTerminal: isTerminal, ); }, padding: menuPadding, @@ -541,6 +543,15 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + MenuEntryBase _terminalAction(BuildContext context) { + return _connectCommonAction( + context, + translate('Terminal'), + isTerminal: true, + ); + } + @protected MenuEntryBase _tcpTunnelingAction(BuildContext context) { return _connectCommonAction( @@ -892,6 +903,7 @@ class RecentPeerCard extends BasePeerCard { _connectAction(context), _transferFileAction(context), _viewCameraAction(context), + _terminalAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); @@ -952,6 +964,7 @@ class FavoritePeerCard extends BasePeerCard { _connectAction(context), _transferFileAction(context), _viewCameraAction(context), + _terminalAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -1006,6 +1019,7 @@ class DiscoveredPeerCard extends BasePeerCard { _connectAction(context), _transferFileAction(context), _viewCameraAction(context), + _terminalAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); @@ -1060,6 +1074,7 @@ class AddressBookPeerCard extends BasePeerCard { _connectAction(context), _transferFileAction(context), _viewCameraAction(context), + _terminalAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -1195,6 +1210,7 @@ class MyGroupPeerCard extends BasePeerCard { _connectAction(context), _transferFileAction(context), _viewCameraAction(context), + _terminalAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); @@ -1420,7 +1436,8 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab, {bool isFileTransfer = false, bool isViewCamera = false, bool isTcpTunneling = false, - bool isRDP = false}) async { + bool isRDP = false, + bool isTerminal = false}) async { var password = ''; bool isSharedPassword = false; if (tab == PeerTabIndex.ab) { @@ -1444,6 +1461,7 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab, password: password, isSharedPassword: isSharedPassword, isFileTransfer: isFileTransfer, + isTerminal: isTerminal, isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, isRDP: isRDP); diff --git a/flutter/lib/common/widgets/setting_widgets.dart b/flutter/lib/common/widgets/setting_widgets.dart index cf8e8973727..b57657274a2 100644 --- a/flutter/lib/common/widgets/setting_widgets.dart +++ b/flutter/lib/common/widgets/setting_widgets.dart @@ -243,7 +243,8 @@ List<(String, String)> otherDefaultSettings() { ( 'Use all my displays for the remote session', kKeyUseAllMyDisplaysForTheRemoteSession - ) + ), + ('Keep terminal sessions on disconnect', kOptionTerminalPersistent), ]; return v; diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index c861a09774c..ee05e52a322 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -154,36 +154,38 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { onPressed: () => ffi.cursorModel.reset())); } + // https://github.com/rustdesk/rustdesk/pull/9731 + // Does not work for connection established by "accept". connectWithToken( {bool isFileTransfer = false, bool isViewCamera = false, - bool isTcpTunneling = false}) { + bool isTcpTunneling = false, + bool isTerminal = false}) { final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId); connect(context, id, isFileTransfer: isFileTransfer, isViewCamera: isViewCamera, + isTerminal: isTerminal, isTcpTunneling: isTcpTunneling, connToken: connToken); } - // transferFile if (isDefaultConn && isDesktop) { v.add( TTextMenu( child: Text(translate('Transfer file')), onPressed: () => connectWithToken(isFileTransfer: true)), ); - } - // viewCamera - if (isDefaultConn && isDesktop) { v.add( TTextMenu( child: Text(translate('View camera')), onPressed: () => connectWithToken(isViewCamera: true)), ); - } - // tcpTunneling - if (isDefaultConn && isDesktop) { + v.add( + TTextMenu( + child: Text(translate('Terminal')), + onPressed: () => connectWithToken(isTerminal: true)), + ); v.add( TTextMenu( child: Text(translate('TCP tunneling')), diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index a3063d9c7c2..d608df83aca 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -27,7 +27,6 @@ const String kPlatformAdditionsAmyuniVirtualDisplays = const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard"; const String kPlatformAdditionsSupportedPrivacyModeImpl = "supported_privacy_mode_impl"; -const String kPlatformAdditionsSupportViewCamera = "support_view_camera"; const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformLinux = "Linux"; @@ -47,6 +46,7 @@ const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; const String kAppTypeDesktopViewCamera = "view camera"; const String kAppTypeDesktopPortForward = "port forward"; +const String kAppTypeDesktopTerminal = "terminal"; const String kWindowMainWindowOnTop = "main_window_on_top"; const String kWindowGetWindowInfo = "get_window_info"; @@ -62,6 +62,7 @@ const String kWindowEventNewRemoteDesktop = "new_remote_desktop"; const String kWindowEventNewFileTransfer = "new_file_transfer"; const String kWindowEventNewViewCamera = "new_view_camera"; const String kWindowEventNewPortForward = "new_port_forward"; +const String kWindowEventNewTerminal = "new_terminal"; const String kWindowEventActiveSession = "active_session"; const String kWindowEventActiveDisplaySession = "active_display_session"; const String kWindowEventGetRemoteList = "get_remote_list"; @@ -103,6 +104,8 @@ const String kOptionEnableClipboard = "enable-clipboard"; const String kOptionEnableFileTransfer = "enable-file-transfer"; const String kOptionEnableAudio = "enable-audio"; const String kOptionEnableCamera = "enable-camera"; +const String kOptionEnableTerminal = "enable-terminal"; +const String kOptionTerminalPersistent = "terminal-persistent"; const String kOptionEnableTunnel = "enable-tunnel"; const String kOptionEnableRemoteRestart = "enable-remote-restart"; const String kOptionEnableBlockInput = "enable-block-input"; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 6089603783f..41553b8db2d 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -327,10 +327,15 @@ class _ConnectionPageState extends State /// Callback for the connect button. /// Connects to the selected peer. - void onConnect({bool isFileTransfer = false, bool isViewCamera = false}) { + void onConnect( + {bool isFileTransfer = false, + bool isViewCamera = false, + bool isTerminal = false}) { var id = _idController.id; connect(context, id, - isFileTransfer: isFileTransfer, isViewCamera: isViewCamera); + isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, + isTerminal: isTerminal); } /// UI for the remote ID TextField. @@ -527,22 +532,23 @@ class _ConnectionPageState extends State borderRadius: BorderRadius.circular(8), ), child: Center( - child: Obx(() { - var offset = Offset(0, 0); - return InkWell( - child: _menuOpen.value - ? Transform.rotate( - angle: pi, - child: Icon(IconFont.more, size: 14), - ) - : Icon(IconFont.more, size: 14), - onTapDown: (e) { - offset = e.globalPosition; - }, - onTap: () async { - _menuOpen.value = true; - final x = offset.dx; - final y = offset.dy; + child: StatefulBuilder( + builder: (context, setState) { + var offset = Offset(0, 0); + return Obx(() => InkWell( + child: _menuOpen.value + ? Transform.rotate( + angle: pi, + child: Icon(IconFont.more, size: 14), + ) + : Icon(IconFont.more, size: 14), + onTapDown: (e) { + offset = e.globalPosition; + }, + onTap: () async { + _menuOpen.value = true; + final x = offset.dx; + final y = offset.dy; await mod_menu .showMenu( context: context, @@ -556,6 +562,10 @@ class _ConnectionPageState extends State 'View camera', () => onConnect(isViewCamera: true) ), + ( + 'Terminal', + () => onConnect(isTerminal: true) + ), ] .map((e) => MenuEntryButton( childBuilder: (TextStyle? style) => Text( @@ -583,8 +593,9 @@ class _ConnectionPageState extends State _menuOpen.value = false; }); }, - ); - }), + )); + }, + ), ), ), ]), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 07421d14fae..0f302d8e148 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -786,6 +786,7 @@ class _DesktopHomePageState extends State call.arguments['id'], isFileTransfer: call.arguments['isFileTransfer'], isViewCamera: call.arguments['isViewCamera'], + isTerminal: call.arguments['isTerminal'], isTcpTunneling: call.arguments['isTcpTunneling'], isRDP: call.arguments['isRDP'], password: call.arguments['password'], diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 455ab6c11ac..9d182f9f895 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1011,6 +1011,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { enabled: enabled, fakeValue: fakeValue), _OptionCheckBox(context, 'Enable camera', kOptionEnableCamera, enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable terminal', kOptionEnableTerminal, + enabled: enabled, fakeValue: fakeValue), _OptionCheckBox( context, 'Enable TCP tunneling', kOptionEnableTunnel, enabled: enabled, fakeValue: fakeValue), diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index ec081d57428..4ee29756fc2 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -355,6 +355,7 @@ Widget buildConnectionCard(Client client) { _CmHeader(client: client), client.type_() == ClientType.file || client.type_() == ClientType.portForward || + client.type_() == ClientType.terminal || client.disconnected ? Offstage() : _PrivilegeBoard(client: client), @@ -499,7 +500,36 @@ class _CmHeaderState extends State<_CmHeader> "(${client.peerId})", style: TextStyle(color: Colors.white, fontSize: 14), ), - ).marginOnly(bottom: 10.0), + ), + if (client.type_() == ClientType.terminal) + FittedBox( + child: Text( + translate("Terminal"), + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ), + if (client.type_() == ClientType.file) + FittedBox( + child: Text( + translate("File Transfer"), + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ), + if (client.type_() == ClientType.camera) + FittedBox( + child: Text( + translate("View Camera"), + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ), + if (client.portForward.isNotEmpty) + FittedBox( + child: Text( + "Port Forward: ${client.portForward}", + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ), + SizedBox(height: 10.0), FittedBox( child: Row( children: [ diff --git a/flutter/lib/desktop/pages/terminal_connection_manager.dart b/flutter/lib/desktop/pages/terminal_connection_manager.dart new file mode 100644 index 00000000000..91b8baa9751 --- /dev/null +++ b/flutter/lib/desktop/pages/terminal_connection_manager.dart @@ -0,0 +1,98 @@ +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import '../../models/model.dart'; + +/// Manages terminal connections to ensure one FFI instance per peer +class TerminalConnectionManager { + static final Map _connections = {}; + static final Map _connectionRefCount = {}; + + // Track service IDs per peer + static final Map _serviceIds = {}; + + /// Get or create an FFI instance for a peer + static FFI getConnection({ + required String peerId, + required String? password, + required bool? isSharedPassword, + required bool? forceRelay, + required String? connToken, + }) { + final existingFfi = _connections[peerId]; + if (existingFfi != null && !existingFfi.closed) { + // Increment reference count + _connectionRefCount[peerId] = (_connectionRefCount[peerId] ?? 0) + 1; + debugPrint('[TerminalConnectionManager] Reusing existing connection for peer $peerId. Reference count: ${_connectionRefCount[peerId]}'); + return existingFfi; + } + + // Create new FFI instance for first terminal + debugPrint('[TerminalConnectionManager] Creating new terminal connection for peer $peerId'); + final ffi = FFI(null); + ffi.start( + peerId, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay, + connToken: connToken, + isTerminal: true, + ); + + _connections[peerId] = ffi; + _connectionRefCount[peerId] = 1; + + // Register the FFI instance with Get for dependency injection + Get.put(ffi, tag: 'terminal_$peerId'); + + debugPrint('[TerminalConnectionManager] New connection created. Total connections: ${_connections.length}'); + return ffi; + } + + /// Release a connection reference + static void releaseConnection(String peerId) { + final refCount = _connectionRefCount[peerId] ?? 0; + debugPrint('[TerminalConnectionManager] Releasing connection for peer $peerId. Current ref count: $refCount'); + + if (refCount <= 1) { + // Last reference, close the connection + final ffi = _connections[peerId]; + if (ffi != null) { + debugPrint('[TerminalConnectionManager] Closing connection for peer $peerId (last reference)'); + ffi.close(); + _connections.remove(peerId); + _connectionRefCount.remove(peerId); + Get.delete(tag: 'terminal_$peerId'); + } + } else { + // Decrement reference count + _connectionRefCount[peerId] = refCount - 1; + debugPrint('[TerminalConnectionManager] Connection still in use. New ref count: ${_connectionRefCount[peerId]}'); + } + } + + /// Check if a connection exists for a peer + static bool hasConnection(String peerId) { + final ffi = _connections[peerId]; + return ffi != null && !ffi.closed; + } + + /// Get existing connection without creating new one + static FFI? getExistingConnection(String peerId) { + return _connections[peerId]; + } + + /// Get connection count for debugging + static int getConnectionCount() => _connections.length; + + /// Get terminal count for a peer + static int getTerminalCount(String peerId) => _connectionRefCount[peerId] ?? 0; + + /// Get service ID for a peer + static String? getServiceId(String peerId) => _serviceIds[peerId]; + + /// Set service ID for a peer + static void setServiceId(String peerId, String serviceId) { + _serviceIds[peerId] = serviceId; + debugPrint('[TerminalConnectionManager] Service ID for $peerId: $serviceId'); + } +} \ No newline at end of file diff --git a/flutter/lib/desktop/pages/terminal_page.dart b/flutter/lib/desktop/pages/terminal_page.dart new file mode 100644 index 00000000000..f28545415ee --- /dev/null +++ b/flutter/lib/desktop/pages/terminal_page.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/terminal_model.dart'; +import 'package:xterm/xterm.dart'; +import 'terminal_connection_manager.dart'; + +class TerminalPage extends StatefulWidget { + const TerminalPage({ + Key? key, + required this.id, + required this.password, + required this.tabController, + required this.isSharedPassword, + required this.terminalId, + this.forceRelay, + this.connToken, + }) : super(key: key); + final String id; + final String? password; + final DesktopTabController tabController; + final bool? forceRelay; + final bool? isSharedPassword; + final String? connToken; + final int terminalId; + + @override + State createState() => _TerminalPageState(); +} + +class _TerminalPageState extends State + with AutomaticKeepAliveClientMixin { + late FFI _ffi; + late TerminalModel _terminalModel; + + @override + void initState() { + super.initState(); + + // Use shared FFI instance from connection manager + _ffi = TerminalConnectionManager.getConnection( + peerId: widget.id, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, + connToken: widget.connToken, + ); + + // Create terminal model with specific terminal ID + _terminalModel = TerminalModel(_ffi, widget.terminalId); + debugPrint( + '[TerminalPage] Terminal model created for terminal ${widget.terminalId}'); + + // Register this terminal model with FFI for event routing + _ffi.registerTerminalModel(widget.terminalId, _terminalModel); + + // Initialize terminal connection + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.tabController.onSelected?.call(widget.id); + + // Check if this is a new connection or additional terminal + // Note: When a connection exists, the ref count will be > 1 after this terminal is added + final isExistingConnection = TerminalConnectionManager.hasConnection(widget.id) && + TerminalConnectionManager.getTerminalCount(widget.id) > 1; + + if (!isExistingConnection) { + // First terminal - show loading dialog, wait for onReady + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + } else { + // Additional terminal - connection already established + // Open the terminal directly + _terminalModel.openTerminal(); + } + }); + } + + @override + void dispose() { + // Unregister terminal model from FFI + _ffi.unregisterTerminalModel(widget.terminalId); + _terminalModel.dispose(); + // Release connection reference instead of closing directly + TerminalConnectionManager.releaseConnection(widget.id); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: TerminalView( + _terminalModel.terminal, + controller: _terminalModel.terminalController, + autofocus: true, + backgroundOpacity: 0.7, + padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0), + onSecondaryTapDown: (details, offset) async { + final selection = _terminalModel.terminalController.selection; + if (selection != null) { + final text = _terminalModel.terminal.buffer.getText(selection); + _terminalModel.terminalController.clearSelection(); + await Clipboard.setData(ClipboardData(text: text)); + } else { + final data = await Clipboard.getData('text/plain'); + final text = data?.text; + if (text != null) { + _terminalModel.terminal.paste(text); + } + } + }, + ), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart new file mode 100644 index 00000000000..ee252910738 --- /dev/null +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -0,0 +1,384 @@ +import 'dart:convert'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:get/get.dart'; + +import '../../models/platform_model.dart'; +import 'terminal_page.dart'; +import 'terminal_connection_manager.dart'; +import '../widgets/material_mod_popup_menu.dart' as mod_menu; +import '../widgets/popup_menu.dart'; +import 'package:bot_toast/bot_toast.dart'; + +class TerminalTabPage extends StatefulWidget { + final Map params; + + const TerminalTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _TerminalTabPageState(params); +} + +class _TerminalTabPageState extends State { + DesktopTabController get tabController => Get.find(); + + static const IconData selectedIcon = Icons.terminal; + static const IconData unselectedIcon = Icons.terminal_outlined; + int _nextTerminalId = 1; + + _TerminalTabPageState(Map params) { + Get.put(DesktopTabController(tabType: DesktopTabType.terminal)); + tabController.onSelected = (id) { + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; + tabController.onRemoved = (_, id) => onRemoveId(id); + final terminalId = params['terminalId'] ?? _nextTerminalId++; + tabController.add(_createTerminalTab( + peerId: params['id'], + terminalId: terminalId, + password: params['password'], + isSharedPassword: params['isSharedPassword'], + forceRelay: params['forceRelay'], + connToken: params['connToken'], + )); + } + + TabInfo _createTerminalTab({ + required String peerId, + required int terminalId, + String? password, + bool? isSharedPassword, + bool? forceRelay, + String? connToken, + }) { + final tabKey = '${peerId}_$terminalId'; + return TabInfo( + key: tabKey, + label: '$peerId #$terminalId', + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () async { + // Close the terminal session first + final ffi = TerminalConnectionManager.getExistingConnection(peerId); + if (ffi != null) { + final terminalModel = ffi.terminalModels[terminalId]; + if (terminalModel != null) { + await terminalModel.closeTerminal(); + } + } + // Then close the tab + tabController.closeBy(tabKey); + }, + page: TerminalPage( + key: ValueKey(tabKey), + id: peerId, + terminalId: terminalId, + password: password, + isSharedPassword: isSharedPassword, + tabController: tabController, + forceRelay: forceRelay, + connToken: connToken, + ), + ); + } + + Widget _tabMenuBuilder(String peerId, CancelFunc cancelFunc) { + final List> menu = []; + const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0); + + // New tab menu item + menu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('New tab'), + style: style, + ), + proc: () { + _addNewTerminal(peerId); + cancelFunc(); + // Also try to close any BotToast overlays + BotToast.cleanAll(); + }, + padding: padding, + )); + + menu.add(MenuEntryDivider()); + + menu.add(MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate('Keep terminal sessions on disconnect'), + getter: () async { + final ffi = Get.find(tag: 'terminal_$peerId'); + return bind.sessionGetToggleOptionSync( + sessionId: ffi.sessionId, + arg: kOptionTerminalPersistent, + ); + }, + setter: (bool v) async { + final ffi = Get.find(tag: 'terminal_$peerId'); + bind.sessionToggleOption( + sessionId: ffi.sessionId, + value: kOptionTerminalPersistent, + ); + }, + padding: padding, + )); + + return mod_menu.PopupMenu( + items: menu + .map((e) => e.build( + context, + const MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight, + ), + )) + .expand((i) => i) + .toList(), + ); + } + + @override + void initState() { + super.initState(); + + // Add keyboard shortcut handler + HardwareKeyboard.instance.addHandler(_handleKeyEvent); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + print( + "[Remote Terminal] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + if (call.method == kWindowEventNewTerminal) { + final args = jsonDecode(call.arguments); + final id = args['id']; + windowOnTop(windowId()); + // Allow multiple terminals for the same connection + final terminalId = args['terminalId'] ?? _nextTerminalId++; + tabController.add(_createTerminalTab( + peerId: id, + terminalId: terminalId, + password: args['password'], + isSharedPassword: args['isSharedPassword'], + forceRelay: args['forceRelay'], + connToken: args['connToken'], + )); + } else if (call.method == "onDestroy") { + tabController.clear(); + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } + }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.Terminal, windowId: windowId()); + }); + } + + @override + void dispose() { + HardwareKeyboard.instance.removeHandler(_handleKeyEvent); + super.dispose(); + } + + bool _handleKeyEvent(KeyEvent event) { + if (event is KeyDownEvent) { + // Use Cmd+T on macOS, Ctrl+Shift+T on other platforms + if (event.logicalKey == LogicalKeyboardKey.keyT) { + if (isMacOS && + HardwareKeyboard.instance.isMetaPressed && + !HardwareKeyboard.instance.isShiftPressed) { + // macOS: Cmd+T (standard for new tab) + _addNewTerminalForCurrentPeer(); + return true; + } else if (!isMacOS && + HardwareKeyboard.instance.isControlPressed && + HardwareKeyboard.instance.isShiftPressed) { + // Other platforms: Ctrl+Shift+T (to avoid conflict with Ctrl+T in terminal) + _addNewTerminalForCurrentPeer(); + return true; + } + } + + // Use Cmd+W on macOS, Ctrl+Shift+W on other platforms + if (event.logicalKey == LogicalKeyboardKey.keyW) { + if (isMacOS && + HardwareKeyboard.instance.isMetaPressed && + !HardwareKeyboard.instance.isShiftPressed) { + // macOS: Cmd+W (standard for close tab) + final currentTab = tabController.state.value.selectedTabInfo; + if (tabController.state.value.tabs.length > 1) { + tabController.closeBy(currentTab.key); + return true; + } + } else if (!isMacOS && + HardwareKeyboard.instance.isControlPressed && + HardwareKeyboard.instance.isShiftPressed) { + // Other platforms: Ctrl+Shift+W (to avoid conflict with Ctrl+W word delete) + final currentTab = tabController.state.value.selectedTabInfo; + if (tabController.state.value.tabs.length > 1) { + tabController.closeBy(currentTab.key); + return true; + } + } + } + + // Use Alt+Left/Right for tab navigation (avoids conflicts) + if (HardwareKeyboard.instance.isAltPressed) { + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + // Previous tab + final currentIndex = tabController.state.value.selected; + if (currentIndex > 0) { + tabController.jumpTo(currentIndex - 1); + } + return true; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + // Next tab + final currentIndex = tabController.state.value.selected; + if (currentIndex < tabController.length - 1) { + tabController.jumpTo(currentIndex + 1); + } + return true; + } + } + + // Check for Cmd/Ctrl + Number (switch to specific tab) + final numberKeys = [ + LogicalKeyboardKey.digit1, + LogicalKeyboardKey.digit2, + LogicalKeyboardKey.digit3, + LogicalKeyboardKey.digit4, + LogicalKeyboardKey.digit5, + LogicalKeyboardKey.digit6, + LogicalKeyboardKey.digit7, + LogicalKeyboardKey.digit8, + LogicalKeyboardKey.digit9, + ]; + + for (int i = 0; i < numberKeys.length; i++) { + if (event.logicalKey == numberKeys[i] && + ((isMacOS && HardwareKeyboard.instance.isMetaPressed) || + (!isMacOS && HardwareKeyboard.instance.isControlPressed))) { + if (i < tabController.length) { + tabController.jumpTo(i); + return true; + } + } + } + } + return false; + } + + void _addNewTerminal(String peerId) { + // Find first tab for this peer to get connection parameters + final firstTab = tabController.state.value.tabs.firstWhere( + (tab) => tab.key.startsWith('$peerId\_'), + ); + if (firstTab.page is TerminalPage) { + final page = firstTab.page as TerminalPage; + final terminalId = _nextTerminalId++; + tabController.add(_createTerminalTab( + peerId: peerId, + terminalId: terminalId, + password: page.password, + isSharedPassword: page.isSharedPassword, + forceRelay: page.forceRelay, + connToken: page.connToken, + )); + } + } + + void _addNewTerminalForCurrentPeer() { + final currentTab = tabController.state.value.selectedTabInfo; + final parts = currentTab.key.split('_'); + if (parts.isNotEmpty) { + final peerId = parts[0]; + _addNewTerminal(peerId); + } + } + + @override + Widget build(BuildContext context) { + final child = Scaffold( + backgroundColor: Theme.of(context).cardColor, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: _buildAddButton(), + selectedBorderColor: MyTheme.accent, + labelGetter: DesktopTab.tablabelGetter, + tabMenuBuilder: (key) { + // Extract peerId from tab key (format: "peerId_terminalId") + final parts = key.split('_'); + if (parts.isEmpty) return Container(); + final peerId = parts[0]; + return _tabMenuBuilder(peerId, () {}); + }, + )); + final tabWidget = isLinux + ? buildVirtualWindowFrame(context, child) + : workaroundWindowBorder( + context, + Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: child, + )); + return isMacOS || kUseCompatibleUiMode + ? tabWidget + : SubWindowDragToResizeArea( + child: tabWidget, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, + windowId: stateGlobal.windowId, + ); + } + + void onRemoveId(String id) { + if (tabController.state.value.tabs.isEmpty) { + WindowController.fromWindowId(windowId()).close(); + } + } + + int windowId() { + return widget.params["windowId"]; + } + + Widget _buildAddButton() { + return ActionIcon( + message: 'New tab', + icon: IconFont.add, + onTap: () { + _addNewTerminalForCurrentPeer(); + }, + isClose: false, + ); + } + + Future handleWindowCloseButton() async { + final connLength = tabController.state.value.tabs.length; + if (connLength <= 1) { + tabController.clear(); + return true; + } else { + final bool res; + if (!option2bool(kOptionEnableConfirmClosingTabs, + bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + tabController.clear(); + } + return res; + } + } +} diff --git a/flutter/lib/desktop/pages/view_camera_page.dart b/flutter/lib/desktop/pages/view_camera_page.dart index d8611401441..a1cc5c8a074 100644 --- a/flutter/lib/desktop/pages/view_camera_page.dart +++ b/flutter/lib/desktop/pages/view_camera_page.dart @@ -515,8 +515,6 @@ class ImagePaint extends StatefulWidget { } class _ImagePaintState extends State { - bool _lastRemoteCursorMoved = false; - String get id => widget.id; RxBool get cursorOverImage => widget.cursorOverImage; Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder; diff --git a/flutter/lib/desktop/screen/desktop_terminal_screen.dart b/flutter/lib/desktop/screen/desktop_terminal_screen.dart new file mode 100644 index 00000000000..301489c8650 --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_terminal_screen.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:provider/provider.dart'; + +import 'package:flutter_hbb/desktop/pages/terminal_tab_page.dart'; + +class DesktopTerminalScreen extends StatelessWidget { + final Map params; + + const DesktopTerminalScreen({Key? key, required this.params}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ], + child: Scaffold( + backgroundColor: isLinux ? Colors.transparent : null, + body: TerminalTabPage( + params: params, + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 0006901736a..c1cc433ad10 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -54,6 +54,7 @@ enum DesktopTabType { fileTransfer, viewCamera, portForward, + terminal, install, } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index f04e142d94b..80a3bff894a 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -14,6 +14,7 @@ import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_view_camera_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; +import 'package:flutter_hbb/desktop/screen/desktop_terminal_screen.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; @@ -91,6 +92,12 @@ Future main(List args) async { kAppTypeDesktopPortForward, ); break; + case WindowType.Terminal: + desktopType = DesktopType.terminal; + runMultiWindow( + argument, + kAppTypeDesktopTerminal, + ); default: break; } @@ -211,6 +218,11 @@ void runMultiWindow( params: argument, ); break; + case kAppTypeDesktopTerminal: + widget = DesktopTerminalScreen( + params: argument, + ); + break; default: // no such appType exit(0); @@ -257,6 +269,9 @@ void runMultiWindow( case kAppTypeDesktopPortForward: await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!); break; + case kAppTypeDesktopTerminal: + await restoreWindowPosition(WindowType.Terminal, windowId: kWindowId!); + break; default: // no such appType exit(0); diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index b837dc276e3..3faf8f8b08e 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -12,11 +12,12 @@ import '../../common/widgets/dialog.dart'; class FileManagerPage extends StatefulWidget { FileManagerPage( - {Key? key, required this.id, this.password, this.isSharedPassword}) + {Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay}) : super(key: key); final String id; final String? password; final bool? isSharedPassword; + final bool? forceRelay; @override State createState() => _FileManagerPageState(); @@ -74,7 +75,8 @@ class _FileManagerPageState extends State { gFFI.start(widget.id, isFileTransfer: true, password: widget.password, - isSharedPassword: widget.isSharedPassword); + isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay); WidgetsBinding.instance.addPostFrameCallback((_) { gFFI.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index 82d7058ab50..e35c8872c75 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -205,13 +205,13 @@ class WebHomePage extends StatelessWidget { } bool isFileTransfer = false; bool isViewCamera = false; + bool isTerminal = 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; @@ -225,6 +225,11 @@ class WebHomePage extends StatelessWidget { id = args[i + 1]; i++; break; + case '--terminal': + isTerminal = true; + id = args[i + 1]; + i++; + break; case '--password': password = args[i + 1]; i++; @@ -234,7 +239,11 @@ class WebHomePage extends StatelessWidget { } } if (id != null) { - connect(context, id, isFileTransfer: isFileTransfer, isViewCamera: isViewCamera, password: password); + connect(context, id, + isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, + isTerminal: isTerminal, + password: password); } } } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 93f49f585f6..b707fd38f0f 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -40,12 +40,13 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) { } class RemotePage extends StatefulWidget { - RemotePage({Key? key, required this.id, this.password, this.isSharedPassword}) + RemotePage({Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay}) : super(key: key); final String id; final String? password; final bool? isSharedPassword; + final bool? forceRelay; @override State createState() => _RemotePageState(id); @@ -89,6 +90,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { widget.id, password: widget.password, isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index e7f022697d7..ed4fe4d98d1 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -17,7 +17,7 @@ import 'home_page.dart'; class ServerPage extends StatefulWidget implements PageShape { @override - final title = translate("Share Screen"); + final title = translate("Share screen"); @override final icon = const Icon(Icons.mobile_screen_share); @@ -649,8 +649,8 @@ class ConnectionManager extends StatelessWidget { children: serverModel.clients .map((client) => PaddingCard( title: translate(client.isFileTransfer - ? "File Connection" - : "Screen Connection"), + ? "Transfer file" + : "Share screen"), titleIcon: client.isFileTransfer ? Icon(Icons.folder_outlined) : Icon(Icons.mobile_screen_share), diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 693bdbd3027..505b0ff04b4 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -815,7 +815,7 @@ class _SettingsState extends State with WidgetsBindingObserver { !outgoingOnly && !hideSecuritySettings) SettingsSection( - title: Text(translate("Share Screen")), + title: Text(translate("Share screen")), tiles: shareScreenTiles, ), if (!bind.isIncomingOnly()) defaultDisplaySection(), diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart new file mode 100644 index 00000000000..d7d17994c3a --- /dev/null +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/terminal_model.dart'; +import 'package:xterm/xterm.dart'; +import '../../desktop/pages/terminal_connection_manager.dart'; + +class TerminalPage extends StatefulWidget { + const TerminalPage({ + Key? key, + required this.id, + required this.password, + required this.isSharedPassword, + this.forceRelay, + this.connToken, + }) : super(key: key); + final String id; + final String? password; + final bool? forceRelay; + final bool? isSharedPassword; + final String? connToken; + final terminalId = 0; + + @override + State createState() => _TerminalPageState(); +} + +class _TerminalPageState extends State + with AutomaticKeepAliveClientMixin { + late FFI _ffi; + late TerminalModel _terminalModel; + + @override + void initState() { + super.initState(); + + debugPrint( + '[TerminalPage] Initializing terminal ${widget.terminalId} for peer ${widget.id}'); + + // Use shared FFI instance from connection manager + _ffi = TerminalConnectionManager.getConnection( + peerId: widget.id, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, + connToken: widget.connToken, + ); + + // Create terminal model with specific terminal ID + _terminalModel = TerminalModel(_ffi, widget.terminalId); + debugPrint( + '[TerminalPage] Terminal model created for terminal ${widget.terminalId}'); + + // Register this terminal model with FFI for event routing + _ffi.registerTerminalModel(widget.terminalId, _terminalModel); + + // Initialize terminal connection + WidgetsBinding.instance.addPostFrameCallback((_) { + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + _ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id); + } + + @override + void dispose() { + // Unregister terminal model from FFI + _ffi.unregisterTerminalModel(widget.terminalId); + _terminalModel.dispose(); + super.dispose(); + TerminalConnectionManager.releaseConnection(widget.id); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: TerminalView( + _terminalModel.terminal, + controller: _terminalModel.terminalController, + autofocus: true, + backgroundOpacity: 0.7, + padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0), + onSecondaryTapDown: (details, offset) async { + final selection = _terminalModel.terminalController.selection; + if (selection != null) { + final text = _terminalModel.terminal.buffer.getText(selection); + _terminalModel.terminalController.clearSelection(); + await Clipboard.setData(ClipboardData(text: text)); + } else { + final data = await Clipboard.getData('text/plain'); + final text = data?.text; + if (text != null) { + _terminalModel.terminal.paste(text); + } + } + }, + ), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart index ac70a2dab8e..1b668673ac8 100644 --- a/flutter/lib/mobile/pages/view_camera_page.dart +++ b/flutter/lib/mobile/pages/view_camera_page.dart @@ -39,12 +39,13 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) { class ViewCameraPage extends StatefulWidget { ViewCameraPage( - {Key? key, required this.id, this.password, this.isSharedPassword}) + {Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay}) : super(key: key); final String id; final String? password; final bool? isSharedPassword; + final bool? forceRelay; @override State createState() => _ViewCameraPageState(id); @@ -88,6 +89,7 @@ class _ViewCameraPageState extends State isViewCamera: true, password: widget.password, isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c28d3c1d637..b151120251c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -23,6 +23,7 @@ import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/desktop_render_texture.dart'; +import 'package:flutter_hbb/models/terminal_model.dart'; import 'package:flutter_hbb/plugin/event.dart'; import 'package:flutter_hbb/plugin/manager.dart'; import 'package:flutter_hbb/plugin/widgets/desc_ui.dart'; @@ -311,6 +312,8 @@ class FfiModel with ChangeNotifier { } else if (name == 'chat_server_mode') { parent.target?.chatModel .receive(int.parse(evt['id'] as String), evt['text'] ?? ''); + } else if (name == 'terminal_response') { + parent.target?.routeTerminalResponse(evt); } else if (name == 'file_dir') { parent.target?.fileModel.receiveFileDir(evt); } else if (name == 'empty_dirs') { @@ -1076,9 +1079,14 @@ class FfiModel with ChangeNotifier { sessionId: sessionId, arg: kOptionTouchMode) != ''; } - // FIXME: handle ViewCamera ConnType independently. if (connType == ConnType.fileTransfer) { parent.target?.fileModel.onReady(); + } else if (connType == ConnType.terminal) { + // Call onReady on all registered terminal models + final models = parent.target?._terminalModels.values ?? []; + for (final model in models) { + model.onReady(); + } } else if (connType == ConnType.defaultConn || connType == ConnType.viewCamera) { List newDisplays = []; @@ -2828,7 +2836,14 @@ class ElevationModel with ChangeNotifier { } // The index values of `ConnType` are same as rust protobuf. -enum ConnType { defaultConn, fileTransfer, portForward, rdp, viewCamera } +enum ConnType { + defaultConn, + fileTransfer, + portForward, + rdp, + viewCamera, + terminal +} /// Flutter state manager and data communication with the Rust core. class FFI { @@ -2863,6 +2878,12 @@ class FFI { late final Peers favoritePeersModel; // global late final Peers lanPeersModel; // global + // Terminal model registry for multiple terminals + final Map _terminalModels = {}; + + // Getter for terminal models + Map get terminalModels => _terminalModels; + FFI(SessionID? sId) { sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId); imageModel = ImageModel(WeakReference(this)); @@ -2910,6 +2931,7 @@ class FFI { bool isViewCamera = false, bool isPortForward = false, bool isRdp = false, + bool isTerminal = false, String? switchUuid, String? password, bool? isSharedPassword, @@ -2925,7 +2947,10 @@ class FFI { assert( (!(isPortForward && isViewCamera)) && (!(isViewCamera && isPortForward)) && - (!(isPortForward && isFileTransfer)), + (!(isPortForward && isFileTransfer)) && + (!(isTerminal && isFileTransfer)) && + (!(isTerminal && isViewCamera)) && + (!(isTerminal && isPortForward)), 'more than one connect type'); if (isFileTransfer) { connType = ConnType.fileTransfer; @@ -2933,6 +2958,8 @@ class FFI { connType = ConnType.viewCamera; } else if (isPortForward) { connType = ConnType.portForward; + } else if (isTerminal) { + connType = ConnType.terminal; } else { chatModel.resetClientMode(); connType = ConnType.defaultConn; @@ -2953,6 +2980,7 @@ class FFI { isViewCamera: isViewCamera, isPortForward: isPortForward, isRdp: isRdp, + isTerminal: isTerminal, switchUuid: switchUuid ?? '', forceRelay: forceRelay ?? false, password: password ?? '', @@ -3132,6 +3160,11 @@ class FFI { Future close({bool closeSession = true}) async { closed = true; chatModel.close(); + // Close all terminal models + for (final model in _terminalModels.values) { + model.dispose(); + } + _terminalModels.clear(); if (imageModel.image != null && !isWebDesktop) { await setCanvasConfig( sessionId, @@ -3162,6 +3195,27 @@ class FFI { Future invokeMethod(String method, [dynamic arguments]) async { return await platformFFI.invokeMethod(method, arguments); } + + // Terminal model management + void registerTerminalModel(int terminalId, TerminalModel model) { + debugPrint('[FFI] Registering terminal model for terminal $terminalId'); + _terminalModels[terminalId] = model; + } + + void unregisterTerminalModel(int terminalId) { + debugPrint('[FFI] Unregistering terminal model for terminal $terminalId'); + _terminalModels.remove(terminalId); + } + + void routeTerminalResponse(Map evt) { + final int terminalId = evt['terminal_id'] ?? 0; + + // Route to specific terminal model if it exists + final model = _terminalModels[terminalId]; + if (model != null) { + model.handleTerminalResponse(evt); + } + } } const kInvalidResolutionValue = -1; @@ -3266,9 +3320,6 @@ class PeerInfo with ChangeNotifier { bool get isAmyuniIdd => platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd'; - bool get isSupportViewCamera => - platformAdditions[kPlatformAdditionsSupportViewCamera] == true; - Display? tryGetDisplay({int? display}) { if (displays.isEmpty) { return null; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 9c46e079793..c3e6fab71b7 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -613,7 +613,13 @@ class ServerModel with ChangeNotifier { void showLoginDialog(Client client) { showClientDialog( client, - client.isFileTransfer ? "File Connection" : "Screen Connection", + client.isFileTransfer + ? "Transfer file" + : client.isViewCamera + ? "View camera" + : client.isTerminal + ? "Terminal" + : "Share screen", 'Do you accept?', 'android_new_connection_tip', () => sendLoginResponse(client, false), @@ -692,7 +698,7 @@ class ServerModel with ChangeNotifier { void sendLoginResponse(Client client, bool res) async { if (res) { bind.cmLoginRes(connId: client.id, res: res); - if (!client.isFileTransfer) { + if (!client.isFileTransfer && !client.isTerminal) { parent.target?.invokeMethod("start_capture"); } parent.target?.invokeMethod("cancel_notification", client.id); @@ -806,6 +812,7 @@ enum ClientType { file, camera, portForward, + terminal, } class Client { @@ -813,6 +820,7 @@ class Client { bool authorized = false; bool isFileTransfer = false; bool isViewCamera = false; + bool isTerminal = false; String portForward = ""; String name = ""; String peerId = ""; // peer user's id,show at app @@ -839,6 +847,7 @@ class Client { isFileTransfer = json['is_file_transfer']; // TODO: no entry then default. isViewCamera = json['is_view_camera']; + isTerminal = json['is_terminal'] ?? false; portForward = json['port_forward']; name = json['name']; peerId = json['peer_id']; @@ -861,6 +870,7 @@ class Client { data['authorized'] = authorized; data['is_file_transfer'] = isFileTransfer; data['is_view_camera'] = isViewCamera; + data['is_terminal'] = isTerminal; data['port_forward'] = portForward; data['name'] = name; data['peer_id'] = peerId; @@ -883,6 +893,8 @@ class Client { return ClientType.file; } else if (isViewCamera) { return ClientType.camera; + } else if (isTerminal) { + return ClientType.terminal; } else if (portForward.isNotEmpty) { return ClientType.portForward; } else { diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart new file mode 100644 index 00000000000..3284c539bb5 --- /dev/null +++ b/flutter/lib/models/terminal_model.dart @@ -0,0 +1,269 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:xterm/xterm.dart'; + +import 'model.dart'; +import 'platform_model.dart'; + +class TerminalModel with ChangeNotifier { + final String id; // peer id + final FFI parent; + final int terminalId; + late final Terminal terminal; + late final TerminalController terminalController; + + bool _terminalOpened = false; + bool get terminalOpened => _terminalOpened; + + bool _disposed = false; + + final _inputBuffer = []; + + Future _handleInput(String data) async { + if (_terminalOpened) { + // Send user input to remote terminal + try { + await bind.sessionSendTerminalInput( + sessionId: parent.sessionId, + terminalId: terminalId, + data: data, + ); + } catch (e) { + debugPrint('[TerminalModel] Error sending terminal input: $e'); + } + } else { + debugPrint('[TerminalModel] Terminal not opened yet, buffering input'); + _inputBuffer.add(data); + } + } + + TerminalModel(this.parent, [this.terminalId = 0]) : id = parent.id { + terminal = Terminal(maxLines: 10000); + terminalController = TerminalController(); + + // Setup terminal callbacks + terminal.onOutput = _handleInput; + + terminal.onResize = (w, h, pw, ph) async { + // Validate all dimensions before using them + if (w > 0 && h > 0 && pw > 0 && ph > 0) { + debugPrint( + '[TerminalModel] Terminal resized to ${w}x$h (pixel: ${pw}x$ph)'); + if (_terminalOpened) { + // Notify remote terminal of resize + try { + await bind.sessionResizeTerminal( + sessionId: parent.sessionId, + terminalId: terminalId, + rows: h, + cols: w, + ); + } catch (e) { + debugPrint('[TerminalModel] Error resizing terminal: $e'); + } + } + } else { + debugPrint( + '[TerminalModel] Invalid terminal dimensions: ${w}x$h (pixel: ${pw}x$ph)'); + } + }; + } + + void onReady() { + parent.dialogManager.dismissAll(); + + // Fire and forget - don't block onReady + openTerminal().catchError((e) { + debugPrint('[TerminalModel] Error opening terminal: $e'); + }); + } + + Future openTerminal() async { + if (_terminalOpened) return; + // Request the remote side to open a terminal with default shell + // The remote side will decide which shell to use based on its OS + + // Get terminal dimensions, ensuring they are valid + int rows = 24; + int cols = 80; + + if (terminal.viewHeight > 0) { + rows = terminal.viewHeight; + } + if (terminal.viewWidth > 0) { + cols = terminal.viewWidth; + } + + debugPrint( + '[TerminalModel] Opening terminal $terminalId, sessionId: ${parent.sessionId}, size: ${cols}x$rows'); + try { + await bind + .sessionOpenTerminal( + sessionId: parent.sessionId, + terminalId: terminalId, + rows: rows, + cols: cols, + ) + .timeout( + const Duration(seconds: 5), + onTimeout: () { + throw TimeoutException( + 'sessionOpenTerminal timed out after 5 seconds'); + }, + ); + debugPrint('[TerminalModel] sessionOpenTerminal called successfully'); + } catch (e) { + debugPrint('[TerminalModel] Error calling sessionOpenTerminal: $e'); + // Optionally show error to user + if (e is TimeoutException) { + terminal.write('Failed to open terminal: Connection timeout\r\n'); + } + } + } + + Future closeTerminal() async { + if (_terminalOpened) { + try { + await bind + .sessionCloseTerminal( + sessionId: parent.sessionId, + terminalId: terminalId, + ) + .timeout( + const Duration(seconds: 3), + onTimeout: () { + throw TimeoutException( + 'sessionCloseTerminal timed out after 3 seconds'); + }, + ); + debugPrint('[TerminalModel] sessionCloseTerminal called successfully'); + } catch (e) { + debugPrint('[TerminalModel] Error calling sessionCloseTerminal: $e'); + // Continue with cleanup even if close fails + } + _terminalOpened = false; + notifyListeners(); + } + } + + void handleTerminalResponse(Map evt) { + final String? type = evt['type']; + final int evtTerminalId = evt['terminal_id'] ?? 0; + + // Only handle events for this terminal + if (evtTerminalId != terminalId) { + debugPrint( + '[TerminalModel] Ignoring event for terminal $evtTerminalId (not mine)'); + return; + } + + switch (type) { + case 'opened': + _handleTerminalOpened(evt); + break; + case 'data': + _handleTerminalData(evt); + break; + case 'closed': + _handleTerminalClosed(evt); + break; + case 'error': + _handleTerminalError(evt); + break; + } + } + + void _handleTerminalOpened(Map evt) { + final bool success = evt['success'] ?? false; + final String message = evt['message'] ?? ''; + final String? serviceId = evt['service_id']; + + debugPrint( + '[TerminalModel] Terminal opened response: success=$success, message=$message, service_id=$serviceId'); + + if (success) { + _terminalOpened = true; + + // Service ID is now saved on the Rust side in handle_terminal_response + + // Process any buffered input + _processBufferedInputAsync().then((_) { + notifyListeners(); + }).catchError((e) { + debugPrint('[TerminalModel] Error processing buffered input: $e'); + notifyListeners(); + }); + } else { + terminal.write('Failed to open terminal: $message\r\n'); + } + } + + Future _processBufferedInputAsync() async { + final buffer = List.from(_inputBuffer); + _inputBuffer.clear(); + + for (final data in buffer) { + try { + await bind.sessionSendTerminalInput( + sessionId: parent.sessionId, + terminalId: terminalId, + data: data, + ); + } catch (e) { + debugPrint('[TerminalModel] Error sending buffered input: $e'); + } + } + } + + void _handleTerminalData(Map evt) { + final data = evt['data']; + + if (data != null) { + try { + String text = ''; + if (data is String) { + // Try to decode as base64 first + try { + final bytes = base64Decode(data); + text = utf8.decode(bytes); + } catch (e) { + // If base64 decode fails, treat as plain text + text = data; + } + } else if (data is List) { + // Handle if data comes as byte array + text = utf8.decode(List.from(data)); + } else { + debugPrint('[TerminalModel] Unknown data type: ${data.runtimeType}'); + return; + } + + terminal.write(text); + } catch (e) { + debugPrint('[TerminalModel] Failed to process terminal data: $e'); + } + } + } + + void _handleTerminalClosed(Map evt) { + final int exitCode = evt['exit_code'] ?? 0; + terminal.write('\r\nTerminal closed with exit code: $exitCode\r\n'); + _terminalOpened = false; + notifyListeners(); + } + + void _handleTerminalError(Map evt) { + final String message = evt['message'] ?? 'Unknown error'; + terminal.write('\r\nTerminal error: $message\r\n'); + } + + @override + void dispose() { + if (_disposed) return; + _disposed = true; + // Terminal cleanup is handled server-side when service closes + super.dispose(); + } +} diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 4e848ea7c9e..a7b06b5c79d 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -17,6 +17,7 @@ enum WindowType { FileTransfer, ViewCamera, PortForward, + Terminal, Unknown } @@ -33,6 +34,8 @@ extension Index on int { return WindowType.ViewCamera; case 4: return WindowType.PortForward; + case 5: + return WindowType.Terminal; default: return WindowType.Unknown; } @@ -61,6 +64,7 @@ class RustDeskMultiWindowManager { final List _fileTransferWindows = List.empty(growable: true); final List _viewCameraWindows = List.empty(growable: true); final List _portForwardWindows = List.empty(growable: true); + final List _terminalWindows = List.empty(growable: true); moveTabToNewWindow(int windowId, String peerId, String sessionId, WindowType windowType) async { @@ -343,6 +347,32 @@ class RustDeskMultiWindowManager { ); } + Future newTerminal( + String remoteId, { + String? password, + bool? isSharedPassword, + bool? forceRelay, + String? connToken, + }) async { + // Terminal windows should always create new windows, not reuse + // This avoids the MissingPluginException when trying to invoke + // new_terminal on an inactive window + var params = { + "type": WindowType.Terminal.index, + "id": remoteId, + "password": password, + "forceRelay": forceRelay, + "isSharedPassword": isSharedPassword, + "connToken": connToken, + }; + final msg = jsonEncode(params); + + // Always create a new window for terminal + final windowId = await newSessionWindow( + WindowType.Terminal, remoteId, msg, _terminalWindows, false); + return MultiWindowCallResult(windowId, null); + } + Future call( WindowType type, String methodName, dynamic args) async { final wnds = _findWindowsByType(type); @@ -373,6 +403,8 @@ class RustDeskMultiWindowManager { return _viewCameraWindows; case WindowType.PortForward: return _portForwardWindows; + case WindowType.Terminal: + return _terminalWindows; case WindowType.Unknown: break; } @@ -395,6 +427,8 @@ class RustDeskMultiWindowManager { case WindowType.PortForward: _portForwardWindows.clear(); break; + case WindowType.Terminal: + _terminalWindows.clear(); case WindowType.Unknown: break; } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 0ec3e076f53..f1839c6305d 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -81,6 +81,7 @@ class RustdeskImpl { required bool isViewCamera, required bool isPortForward, required bool isRdp, + required bool isTerminal, required String switchUuid, required bool forceRelay, required String password, @@ -94,7 +95,8 @@ class RustdeskImpl { 'password': password, 'is_shared_password': isSharedPassword, 'isFileTransfer': isFileTransfer, - 'isViewCamera': isViewCamera + 'isViewCamera': isViewCamera, + 'isTerminal': isTerminal }) ]); } @@ -1911,5 +1913,63 @@ class RustdeskImpl { throw UnimplementedError("sessionTakeScreenshot"); } + Future sessionOpenTerminal( + {required UuidValue sessionId, + required int terminalId, + required int rows, + required int cols, + dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'open_terminal', + jsonEncode({ + 'terminal_id': terminalId, + 'rows': rows, + 'cols': cols, + }) + ])); + } + + Future sessionSendTerminalInput( + {required UuidValue sessionId, + required int terminalId, + required String data, + dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'send_terminal_input', + jsonEncode({ + 'terminal_id': terminalId, + 'data': data, + }) + ])); + } + + Future sessionResizeTerminal( + {required UuidValue sessionId, + required int terminalId, + required int rows, + required int cols, + dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'resize_terminal', + jsonEncode({ + 'terminal_id': terminalId, + 'rows': rows, + 'cols': cols, + }) + ])); + } + + Future sessionCloseTerminal( + {required UuidValue sessionId, + required int terminalId, + dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'close_terminal', + jsonEncode({ + 'terminal_id': terminalId, + }) + ])); + } + void dispose() {} } diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index a9f3c7388cf..0083448428e 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -10,6 +10,11 @@ PODS: - flutter_custom_cursor (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) + - FMDB (2.7.12): + - FMDB/standard (= 2.7.12) + - FMDB/Core (2.7.12) + - FMDB/standard (2.7.12): + - FMDB/Core - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): @@ -17,9 +22,9 @@ PODS: - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS - - sqflite (0.0.3): - - Flutter + - sqflite (0.0.2): - FlutterMacOS + - FMDB (>= 2.7.5) - texture_rgba_renderer (0.0.1): - FlutterMacOS - uni_links_desktop (0.0.1): @@ -46,7 +51,7 @@ DEPENDENCIES: - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) - texture_rgba_renderer (from `Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos`) - uni_links_desktop (from `Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -55,6 +60,10 @@ DEPENDENCIES: - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) +SPEC REPOS: + trunk: + - FMDB + EXTERNAL SOURCES: desktop_drop: :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos @@ -75,7 +84,7 @@ EXTERNAL SOURCES: screen_retriever: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos texture_rgba_renderer: :path: Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos uni_links_desktop: @@ -92,24 +101,25 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos SPEC CHECKSUMS: - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 - device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f - file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 - flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + desktop_multi_window: 93667594ccc4b88d91a97972fd3b1b89667fa80a + device_info_plus: b0fafc687fb901e2af612763340f1b0d4352f8e5 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flutter_custom_cursor: 37e588711a2746f5cf48adb58b582cacff11c0c6 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c - screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2 - uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 - url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 - video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 - wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 - window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 + FMDB: 728731dd336af3936ce00f91d9d8495f5718a0e6 + package_info_plus: 122abb51244f66eead59ce7c9c200d6b53111779 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + screen_retriever: 4f97c103641aab8ce183fa5af3b87029df167936 + sqflite: c73556b2499b92f0b6e6946abe4a4084510cdf90 + texture_rgba_renderer: 6661f577ea5d4990e964c7e3840e544ac798e6da + uni_links_desktop: 34322c2646e4c9abc69b62e1865f9782d2850ba2 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b + wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497 + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9 PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 24a1897ad2d..aba6c7879f6 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,10 +5,15 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" after_layout: dependency: transitive description: @@ -21,10 +26,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.7.0" animations: dependency: transitive description: @@ -45,18 +50,18 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" auto_size_text: dependency: "direct main" description: @@ -69,10 +74,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: @@ -85,10 +90,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" bot_toast: dependency: "direct main" description: @@ -125,10 +130,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -141,18 +146,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.2" built_collection: dependency: transitive description: @@ -165,10 +170,10 @@ packages: dependency: transitive description: name: built_value - sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.10.1" cached_network_image: dependency: transitive description: @@ -189,10 +194,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: @@ -205,10 +210,10 @@ packages: dependency: transitive description: name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -221,10 +226,10 @@ packages: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: @@ -237,10 +242,10 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: transitive description: @@ -261,42 +266,42 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" cross_file: 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: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" csslib: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" dart_style: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.7" dash_chat_2: dependency: "direct main" description: @@ -310,10 +315,10 @@ packages: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" debounce_throttle: dependency: "direct main" description: @@ -335,7 +340,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "4f562ab49d289cfa36bfda7cff12746ec0200033" + resolved-ref: b47e8385e5a75d38319ad706a64b0ead3108b093 url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -351,10 +356,10 @@ packages: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.2" draggable_float_widget: dependency: "direct main" description: @@ -380,6 +385,14 @@ packages: url: "https://github.com/rustdesk-org/dynamic_layouts.git" source: git version: "0.0.1+1" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" extended_text: dependency: "direct main" description: @@ -392,10 +405,10 @@ packages: dependency: transitive description: name: extended_text_library - sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829" + sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba" url: "https://pub.dev" source: hosted - version: "12.0.0" + version: "12.0.1" external_path: dependency: "direct main" description: @@ -440,18 +453,18 @@ packages: dependency: transitive description: name: file_selector_linux - sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" url: "https://pub.dev" source: hosted - version: "0.9.2+1" + version: "0.9.3+2" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" url: "https://pub.dev" source: hosted - version: "0.9.3+3" + version: "0.9.4+2" file_selector_platform_interface: dependency: transitive description: @@ -464,34 +477,34 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.3+4" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flex_color_picker: dependency: "direct main" description: name: flex_color_picker - sha256: "0871edc170153cfc3de316d30625f40a85daecfa76ce541641f3cc0ec7757cbf" + sha256: "12dc855ae8ef5491f529b1fc52c655f06dcdf4114f1f7fdecafa41eec2ec8d79" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.6.0" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f" + sha256: "7639d2c86268eff84a909026eb169f008064af0fb3696a651b24b0fa24a40334" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "3.4.1" flutter: dependency: "direct main" description: flutter @@ -608,7 +621,7 @@ packages: source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: - dependency: transitive + dependency: "direct overridden" description: name: flutter_plugin_android_lifecycle sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da @@ -627,10 +640,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -640,74 +653,74 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" + sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.5.7" 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: name: get - sha256: e4e7335ede17452b391ed3b2ede016545706c01a02292a6c97619705e7d2a85e + sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 url: "https://pub.dev" source: hosted - version: "4.6.6" + version: "4.7.2" glob: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" html: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.6" http: dependency: "direct main" description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: @@ -728,10 +741,10 @@ packages: dependency: "direct main" description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.3.0" image_picker: dependency: "direct main" description: @@ -744,50 +757,50 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" + sha256: "82652a75e3dd667a91187769a6a2cc81bd8c111bbead698d8e938d2b63e5e89a" url: "https://pub.dev" source: hosted - version: "0.8.9+3" + version: "0.8.12+21" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "3.0.6" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" url: "https://pub.dev" source: hosted - version: "0.8.9+1" + version: "0.8.12+2" image_picker_linux: dependency: transitive description: name: image_picker_linux - sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" url: "https://pub.dev" source: hosted - version: "0.2.1+1" + version: "0.2.1+2" image_picker_macos: dependency: transitive description: name: image_picker_macos - sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" url: "https://pub.dev" source: hosted - version: "0.2.1+1" + version: "0.2.1+2" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.10.1" image_picker_windows: dependency: transitive description: @@ -808,10 +821,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -824,10 +837,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: @@ -840,18 +853,26 @@ packages: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -872,10 +893,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" nested: dependency: transitive description: @@ -888,18 +909,18 @@ 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: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" package_info_plus: dependency: "direct main" description: @@ -936,34 +957,34 @@ packages: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -984,10 +1005,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: @@ -1000,10 +1021,10 @@ packages: dependency: "direct main" description: name: percent_indicator - sha256: c37099ad833a883c9d71782321cb65c3a848c21b6939b6185f0ff6640d05814c + sha256: "157d29133bbc6ecb11f923d36e7960a96a3f28837549a20b65e5135729f0f9fd" url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.2.5" petitparser: dependency: transitive description: @@ -1016,10 +1037,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -1040,50 +1061,50 @@ packages: dependency: "direct main" description: name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.4.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: "7a990c68d33882b642214c351f66492d9a738afa4226a098ab70642357337fa2" url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "3.16.0" qr: dependency: transitive description: name: qr - sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" qr_code_scanner: dependency: "direct main" description: @@ -1104,10 +1125,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: @@ -1152,10 +1173,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: @@ -1189,82 +1210,82 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sqflite: - dependency: transitive + dependency: "direct main" description: name: sqflite - sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + sha256: a9a8c6dfdf315f87f2a23a7bad2b60c8d5af0f88a5fde92cf9205202770c2753 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.2.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4+6" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+3" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.6" texture_rgba_renderer: dependency: "direct main" description: @@ -1278,18 +1299,18 @@ packages: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" toggle_switch: 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: @@ -1302,10 +1323,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" uni_links: dependency: "direct main" description: @@ -1367,50 +1388,50 @@ packages: dependency: "direct main" description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.2" 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: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.4" uuid: dependency: "direct main" description: @@ -1423,26 +1444,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.18" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.16" vector_math: dependency: transitive description: @@ -1455,42 +1476,42 @@ packages: dependency: transitive description: name: video_player - sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2 + sha256: "7d78f0cfaddc8c19d4cb2d3bebe1bfef11f2103b0a03e5398b303a1bf65eeb14" url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.9.5" video_player_android: dependency: transitive description: name: video_player_android - sha256: "7f8f25d7ad56819a82b2948357f3c3af071f6a678db33833b26ec36bbc221316" + sha256: "391e092ba4abe2f93b3e625bd6b6a6ec7d7414279462c1c0ee42b5ab8d0a0898" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.7.16" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" + sha256: "9ee764e5cd2fc1e10911ae8ad588e1a19db3b6aa9a6eb53c127c42d3a3c3f22f" url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.7.1" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" + sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" video_player_web: dependency: transitive description: name: video_player_web - sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb" + sha256: e8bba2e5d1e159d5048c9a491bb2a7b29c535c612bb7d10c1e21107f5bd365ba url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.3.5" visibility_detector: dependency: "direct main" description: @@ -1503,50 +1524,50 @@ 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: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.3" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" web: 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: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.10.1" win32_registry: dependency: transitive description: @@ -1577,10 +1598,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -1589,30 +1610,46 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + xterm: + dependency: "direct main" + description: + name: xterm + sha256: "168dfedca77cba33fdb6f52e2cd001e9fde216e398e89335c19b524bb22da3a2" + url: "https://pub.dev" + source: hosted + version: "4.0.0" yaml: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" yaml_edit: dependency: transitive description: name: yaml_edit - sha256: "1579d4a0340a83cf9e4d580ea51a16329c916973bffd5bd4b45e911b25d46bfd" + sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.2" + zmodem: + dependency: transitive + description: + name: zmodem + sha256: "3b7e5b29f3a7d8aee472029b05165a68438eff2f3f7766edf13daba1e297adbf" + url: "https://pub.dev" + source: hosted + version: "0.0.6" zxing2: dependency: "direct main" description: name: zxing2 - sha256: a042961441bd400f59595f9125ef5fca4c888daf0ea59c17f41e0e151f8a12b5 + sha256: "2677c49a3b9ca9457cb1d294fd4bd5041cac6aab8cdb07b216ba4e98945c684f" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.4" sdks: dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index ac9b753fc15..03de1a4eb01 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -106,6 +106,8 @@ dependencies: device_info_plus: ^9.1.0 qr_flutter: ^4.1.0 extended_text: 14.0.0 + xterm: 4.0.0 + sqflite: 2.2.0 dev_dependencies: icons_launcher: ^2.0.4 @@ -118,7 +120,8 @@ dev_dependencies: dependency_overrides: intl: ^0.19.0 - + flutter_plugin_android_lifecycle: 2.0.17 + # rerun: flutter pub run flutter_launcher_icons flutter_icons: image_path: "../res/icon.png" @@ -193,4 +196,3 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages - diff --git a/flutter/web/v1/.gitignore b/flutter/web/v1/.gitignore deleted file mode 100644 index 6290a3f6319..00000000000 --- a/flutter/web/v1/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -assets -js/src/gen_js_from_hbb.ts -js/src/message.ts -js/src/rendezvous.ts -ogvjs* -libopus.js -libopus.wasm -yuv-canvas* -node_modules diff --git a/flutter/web/v1/README.md b/flutter/web/v1/README.md deleted file mode 100644 index b9e2fc5c096..00000000000 --- a/flutter/web/v1/README.md +++ /dev/null @@ -1 +0,0 @@ -v1 is not compatible with current Flutter source code. \ No newline at end of file diff --git a/flutter/web/v1/index.html b/flutter/web/v1/index.html deleted file mode 100644 index b466df66225..00000000000 --- a/flutter/web/v1/index.html +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - RustDesk - - - - - - - - - - -
    -
    -
    - - - - - - - - - diff --git a/flutter/web/v1/js/.gitattributes b/flutter/web/v1/js/.gitattributes deleted file mode 100644 index 176a458f94e..00000000000 --- a/flutter/web/v1/js/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -* text=auto diff --git a/flutter/web/v1/js/.gitignore b/flutter/web/v1/js/.gitignore deleted file mode 100644 index 8737dbba591..00000000000 --- a/flutter/web/v1/js/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -node_modules -.DS_Store -dist -dist-ssr -*.local -*log -ogvjs -.vscode -.yarn diff --git a/flutter/web/v1/js/.yarnrc.yml b/flutter/web/v1/js/.yarnrc.yml deleted file mode 100644 index 3186f3f0795..00000000000 --- a/flutter/web/v1/js/.yarnrc.yml +++ /dev/null @@ -1 +0,0 @@ -nodeLinker: node-modules diff --git a/flutter/web/v1/js/gen_js_from_hbb.py b/flutter/web/v1/js/gen_js_from_hbb.py deleted file mode 100755 index cfa95ffe0a1..00000000000 --- a/flutter/web/v1/js/gen_js_from_hbb.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 - -import re -import os -import glob -from tabnanny import check - -def pad_start(s, n, c = ' '): - if len(s) >= n: - return s - return c * (n - len(s)) + s - -def safe_unicode(s): - res = "" - for c in s: - res += r"\u{}".format(pad_start(hex(ord(c))[2:], 4, '0')) - return res - -def main(): - print('export const LANGS = {') - for fn in glob.glob('../../../src/lang/*'): - lang = os.path.basename(fn)[:-3] - if lang == 'template': continue - print(' %s: {'%lang) - for ln in open(fn, encoding='utf-8'): - ln = ln.strip() - if ln.startswith('("'): - toks = ln.split('", "') - assert(len(toks) == 2) - a = toks[0][2:] - b = toks[1][:-3] - print(' "%s": "%s",'%(safe_unicode(a), safe_unicode(b))) - print(' },') - print('}') - check_if_retry = ['', False] - KEY_MAP = ['', False] - for ln in open('../../../src/client.rs', encoding='utf-8'): - ln = ln.strip() - if 'check_if_retry' in ln: - check_if_retry[1] = True - continue - if ln.startswith('}') and check_if_retry[1]: - check_if_retry[1] = False - continue - if check_if_retry[1]: - ln = removeComment(ln) - check_if_retry[0] += ln + '\n' - if 'KEY_MAP' in ln: - KEY_MAP[1] = True - continue - if '.collect' in ln and KEY_MAP[1]: - KEY_MAP[1] = False - continue - if KEY_MAP[1] and ln.startswith('('): - ln = removeComment(ln) - toks = ln.split('", Key::') - assert(len(toks) == 2) - a = toks[0][2:] - b = toks[1].replace('ControlKey(ControlKey::', '').replace("Chr('", '').replace("' as _)),", '').replace(')),', '') - KEY_MAP[0] += ' "%s": "%s",\n'%(a, b) - print() - print('export function checkIfRetry(msgtype: string, title: string, text: string, retry_for_relay: boolean) {') - print(' return %s'%check_if_retry[0].replace('to_lowercase', 'toLowerCase').replace('contains', 'indexOf').replace('!', '').replace('")', '") < 0')) - print(';}') - print() - print('export const KEY_MAP: any = {') - print(KEY_MAP[0]) - print('}') - for ln in open('../../../Cargo.toml', encoding='utf-8'): - if ln.startswith('version ='): - print('export const ' + ln) - - -def removeComment(ln): - return re.sub('\s+\/\/.*$', '', ln) - -main() diff --git a/flutter/web/v1/js/index.html b/flutter/web/v1/js/index.html deleted file mode 100644 index 0ae0a24103f..00000000000 --- a/flutter/web/v1/js/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - Vite App - - -
    - - - diff --git a/flutter/web/v1/js/package.json b/flutter/web/v1/js/package.json deleted file mode 100644 index 15e0e75b89d..00000000000 --- a/flutter/web/v1/js/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "web_hbb", - "version": "1.0.0", - "scripts": { - "dev": "vite", - "build": "./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && ./ts_proto.py && tsc && vite build", - "preview": "vite preview" - }, - "devDependencies": { - "typescript": "^4.4.4", - "vite": "^2.7.2" - }, - "dependencies": { - "fast-sha256": "^1.3.0", - "libsodium": "^0.7.9", - "libsodium-wrappers": "^0.7.9", - "pcm-player": "^0.0.11", - "ts-proto": "^1.101.0", - "wasm-feature-detect": "^1.2.11", - "zstddec": "^0.0.2" - } -} diff --git a/flutter/web/v1/js/src/codec.js b/flutter/web/v1/js/src/codec.js deleted file mode 100644 index 27c9565ec70..00000000000 --- a/flutter/web/v1/js/src/codec.js +++ /dev/null @@ -1,43 +0,0 @@ -// example: https://github.com/rgov/js-theora-decoder/blob/main/index.html -// https://github.com/brion/ogv.js/releases, yarn add has no simd -// dev: copy decoder files from node/ogv/dist/* to project dir -// dist: .... to dist -/* - OGVDemuxerOggW: 'ogv-demuxer-ogg-wasm.js', - OGVDemuxerWebMW: 'ogv-demuxer-webm-wasm.js', - OGVDecoderAudioOpusW: 'ogv-decoder-audio-opus-wasm.js', - OGVDecoderAudioVorbisW: 'ogv-decoder-audio-vorbis-wasm.js', - OGVDecoderVideoTheoraW: 'ogv-decoder-video-theora-wasm.js', - OGVDecoderVideoVP8W: 'ogv-decoder-video-vp8-wasm.js', - OGVDecoderVideoVP8MTW: 'ogv-decoder-video-vp8-mt-wasm.js', - OGVDecoderVideoVP9W: 'ogv-decoder-video-vp9-wasm.js', - OGVDecoderVideoVP9SIMDW: 'ogv-decoder-video-vp9-simd-wasm.js', - OGVDecoderVideoVP9MTW: 'ogv-decoder-video-vp9-mt-wasm.js', - OGVDecoderVideoVP9SIMDMTW: 'ogv-decoder-video-vp9-simd-mt-wasm.js', - OGVDecoderVideoAV1W: 'ogv-decoder-video-av1-wasm.js', - OGVDecoderVideoAV1SIMDW: 'ogv-decoder-video-av1-simd-wasm.js', - OGVDecoderVideoAV1MTW: 'ogv-decoder-video-av1-mt-wasm.js', - OGVDecoderVideoAV1SIMDMTW: 'ogv-decoder-video-av1-simd-mt-wasm.js', -*/ -import { simd } from "wasm-feature-detect"; - -export async function loadVp9(callback) { - // Multithreading is used only if `options.threading` is true. - // This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs, - // currently available in Firefox and Chrome with experimental flags enabled. - // 所有主流浏览器均默认于2018年1月5日禁用SharedArrayBuffer - const isSIMD = await simd(); - console.log('isSIMD: ' + isSIMD); - window.OGVLoader.loadClass( - isSIMD ? "OGVDecoderVideoVP9SIMDW" : "OGVDecoderVideoVP9W", - (videoCodecClass) => { - window.videoCodecClass = videoCodecClass; - videoCodecClass({ videoFormat: {} }).then((decoder) => { - decoder.init(() => { - callback(decoder); - }) - }) - }, - { worker: true, threading: true } - ); -} \ No newline at end of file diff --git a/flutter/web/v1/js/src/common.ts b/flutter/web/v1/js/src/common.ts deleted file mode 100644 index 8da049a4db5..00000000000 --- a/flutter/web/v1/js/src/common.ts +++ /dev/null @@ -1,77 +0,0 @@ -import * as zstd from "zstddec"; -import { KeyEvent, controlKeyFromJSON, ControlKey } from "./message"; -import { KEY_MAP, LANGS } from "./gen_js_from_hbb"; - -let decompressor: zstd.ZSTDDecoder; - -export async function initZstd() { - const tmp = new zstd.ZSTDDecoder(); - await tmp.init(); - console.log("zstd ready"); - decompressor = tmp; -} - -export async function decompress(compressedArray: Uint8Array) { - const MAX = 1024 * 1024 * 64; - const MIN = 1024 * 1024; - let n = 30 * compressedArray.length; - if (n > MAX) { - n = MAX; - } - if (n < MIN) { - n = MIN; - } - try { - if (!decompressor) { - await initZstd(); - } - return decompressor.decode(compressedArray, n); - } catch (e) { - console.error("decompress failed: " + e); - return undefined; - } -} - -const LANG = getLang(); - -export function translate(locale: string, text: string): string { - const lang = LANG || locale.substring(locale.length - 2).toLowerCase(); - let en = LANGS.en as any; - let dict = (LANGS as any)[lang]; - if (!dict) dict = en; - let res = dict[text]; - if (!res && lang != "en") res = en[text]; - return res || text; -} - -const zCode = "z".charCodeAt(0); -const aCode = "a".charCodeAt(0); - -export function mapKey(name: string, isDesktop: Boolean) { - const tmp = KEY_MAP[name] || name; - if (tmp.length == 1) { - const chr = tmp.charCodeAt(0); - if (!isDesktop && (chr > zCode || chr < aCode)) - return KeyEvent.fromPartial({ unicode: chr }); - else return KeyEvent.fromPartial({ chr }); - } - const control_key = controlKeyFromJSON(tmp); - if (control_key == ControlKey.UNRECOGNIZED) { - console.error("Unknown control key " + tmp); - } - return KeyEvent.fromPartial({ control_key }); -} - -export async function sleep(ms: number) { - await new Promise((r) => setTimeout(r, ms)); -} - -function getLang(): string { - try { - const queryString = window.location.search; - const urlParams = new URLSearchParams(queryString); - return urlParams.get("lang") || ""; - } catch (e) { - return ""; - } -} diff --git a/flutter/web/v1/js/src/connection.ts b/flutter/web/v1/js/src/connection.ts deleted file mode 100644 index b0c479c90cc..00000000000 --- a/flutter/web/v1/js/src/connection.ts +++ /dev/null @@ -1,773 +0,0 @@ -import Websock from "./websock"; -import * as message from "./message.js"; -import * as rendezvous from "./rendezvous.js"; -import { loadVp9 } from "./codec"; -import * as sha256 from "fast-sha256"; -import * as globals from "./globals"; -import { decompress, mapKey, sleep } from "./common"; - -const PORT = 21116; -const HOSTS = [ - "rs-sg.rustdesk.com", - "rs-cn.rustdesk.com", - "rs-us.rustdesk.com", -]; -let HOST = localStorage.getItem("rendezvous-server") || HOSTS[0]; -const SCHEMA = "ws://"; - -type MsgboxCallback = (type: string, title: string, text: string) => void; -type DrawCallback = (data: Uint8Array) => void; -//const cursorCanvas = document.createElement("canvas"); - -export default class Connection { - _msgs: any[]; - _ws: Websock | undefined; - _interval: any; - _id: string; - _hash: message.Hash | undefined; - _msgbox: MsgboxCallback; - _draw: DrawCallback; - _peerInfo: message.PeerInfo | undefined; - _firstFrame: Boolean | undefined; - _videoDecoder: any; - _password: Uint8Array | undefined; - _options: any; - _videoTestSpeed: number[]; - //_cursors: { [name: number]: any }; - - constructor() { - this._msgbox = globals.msgbox; - this._draw = globals.draw; - this._msgs = []; - this._id = ""; - this._videoTestSpeed = [0, 0]; - //this._cursors = {}; - } - - async start(id: string) { - try { - await this._start(id); - } catch (e: any) { - this.msgbox( - "error", - "Connection Error", - e.type == "close" ? "Reset by the peer" : String(e) - ); - } - } - - async _start(id: string) { - if (!this._options) { - this._options = globals.getPeers()[id] || {}; - } - if (!this._password) { - const p = this.getOption("password"); - if (p) { - try { - this._password = Uint8Array.from(JSON.parse("[" + p + "]")); - } catch (e) { - console.error(e); - } - } - } - this._interval = setInterval(() => { - while (this._msgs.length) { - this._ws?.sendMessage(this._msgs[0]); - this._msgs.splice(0, 1); - } - }, 1); - this.loadVideoDecoder(); - const uri = getDefaultUri(); - const ws = new Websock(uri, true); - this._ws = ws; - this._id = id; - console.log( - new Date() + ": Connecting to rendezvous server: " + uri + ", for " + id - ); - await ws.open(); - console.log(new Date() + ": Connected to rendezvous server"); - const conn_type = rendezvous.ConnType.DEFAULT_CONN; - const nat_type = rendezvous.NatType.SYMMETRIC; - const punch_hole_request = rendezvous.PunchHoleRequest.fromPartial({ - id, - licence_key: localStorage.getItem("key") || undefined, - conn_type, - nat_type, - token: localStorage.getItem("access_token") || undefined, - }); - ws.sendRendezvous({ punch_hole_request }); - const msg = (await ws.next()) as rendezvous.RendezvousMessage; - ws.close(); - console.log(new Date() + ": Got relay response"); - const phr = msg.punch_hole_response; - const rr = msg.relay_response; - if (phr) { - if (phr?.other_failure) { - this.msgbox("error", "Error", phr?.other_failure); - return; - } - if (phr.failure != rendezvous.PunchHoleResponse_Failure.UNRECOGNIZED) { - switch (phr?.failure) { - case rendezvous.PunchHoleResponse_Failure.ID_NOT_EXIST: - this.msgbox("error", "Error", "ID does not exist"); - break; - case rendezvous.PunchHoleResponse_Failure.OFFLINE: - this.msgbox("error", "Error", "Remote desktop is offline"); - break; - case rendezvous.PunchHoleResponse_Failure.LICENSE_MISMATCH: - this.msgbox("error", "Error", "Key mismatch"); - break; - case rendezvous.PunchHoleResponse_Failure.LICENSE_OVERUSE: - this.msgbox("error", "Error", "Key overuse"); - break; - } - } - } else if (rr) { - if (!rr.version) { - this.msgbox("error", "Error", "Remote version is low, not support web"); - return; - } - await this.connectRelay(rr); - } - } - - async connectRelay(rr: rendezvous.RelayResponse) { - const pk = rr.pk; - let uri = rr.relay_server; - if (uri) { - uri = getrUriFromRs(uri, true, 2); - } else { - uri = getDefaultUri(true); - } - const uuid = rr.uuid; - console.log(new Date() + ": Connecting to relay server: " + uri); - const ws = new Websock(uri, false); - await ws.open(); - console.log(new Date() + ": Connected to relay server"); - this._ws = ws; - const request_relay = rendezvous.RequestRelay.fromPartial({ - licence_key: localStorage.getItem("key") || undefined, - uuid, - }); - ws.sendRendezvous({ request_relay }); - const secure = (await this.secure(pk)) || false; - globals.pushEvent("connection_ready", { secure, direct: false }); - await this.msgLoop(); - } - - async secure(pk: Uint8Array | undefined) { - if (pk) { - const RS_PK = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; - try { - pk = await globals.verify(pk, localStorage.getItem("key") || RS_PK); - if (pk) { - const idpk = message.IdPk.decode(pk); - if (idpk.id == this._id) { - pk = idpk.pk; - } - } - if (pk?.length != 32) { - pk = undefined; - } - } catch (e) { - console.error(e); - pk = undefined; - } - if (!pk) - console.error( - "Handshake failed: invalid public key from rendezvous server" - ); - } - if (!pk) { - // send an empty message out in case server is setting up secure and waiting for first message - const public_key = message.PublicKey.fromPartial({}); - this._ws?.sendMessage({ public_key }); - return; - } - const msg = (await this._ws?.next()) as message.Message; - let signedId: any = msg?.signed_id; - if (!signedId) { - console.error("Handshake failed: invalid message type"); - const public_key = message.PublicKey.fromPartial({}); - this._ws?.sendMessage({ public_key }); - return; - } - try { - signedId = await globals.verify(signedId.id, Uint8Array.from(pk!)); - } catch (e) { - console.error(e); - // fall back to non-secure connection in case pk mismatch - console.error("pk mismatch, fall back to non-secure"); - const public_key = message.PublicKey.fromPartial({}); - this._ws?.sendMessage({ public_key }); - return; - } - const idpk = message.IdPk.decode(signedId); - const id = idpk.id; - const theirPk = idpk.pk; - if (id != this._id!) { - console.error("Handshake failed: sign failure"); - const public_key = message.PublicKey.fromPartial({}); - this._ws?.sendMessage({ public_key }); - return; - } - if (theirPk.length != 32) { - console.error( - "Handshake failed: invalid public box key length from peer" - ); - const public_key = message.PublicKey.fromPartial({}); - this._ws?.sendMessage({ public_key }); - return; - } - const [mySk, asymmetric_value] = globals.genBoxKeyPair(); - const secret_key = globals.genSecretKey(); - const symmetric_value = globals.seal(secret_key, theirPk, mySk); - const public_key = message.PublicKey.fromPartial({ - asymmetric_value, - symmetric_value, - }); - this._ws?.sendMessage({ public_key }); - this._ws?.setSecretKey(secret_key); - console.log("secured"); - return true; - } - - async msgLoop() { - while (true) { - const msg = (await this._ws?.next()) as message.Message; - if (msg?.hash) { - this._hash = msg?.hash; - if (!this._password) - this.msgbox("input-password", "Password Required", ""); - this.login(); - } else if (msg?.test_delay) { - const test_delay = msg?.test_delay; - console.log(test_delay); - if (!test_delay.from_client) { - this._ws?.sendMessage({ test_delay }); - } - } else if (msg?.login_response) { - const r = msg?.login_response; - if (r.error) { - if (r.error == "Wrong Password") { - this._password = undefined; - this.msgbox( - "re-input-password", - r.error, - "Do you want to enter again?" - ); - } else { - this.msgbox("error", "Login Error", r.error); - } - } else if (r.peer_info) { - this.handlePeerInfo(r.peer_info); - } - } else if (msg?.video_frame) { - this.handleVideoFrame(msg?.video_frame!); - } else if (msg?.clipboard) { - const cb = msg?.clipboard; - if (cb.compress) { - const c = await decompress(cb.content); - if (!c) continue; - cb.content = c; - } - try { - globals.copyToClipboard(new TextDecoder().decode(cb.content)); - } catch (e) { - console.error(e); - } - // globals.pushEvent("clipboard", cb); - } else if (msg?.cursor_data) { - const cd = msg?.cursor_data; - const c = await decompress(cd.colors); - if (!c) continue; - cd.colors = c; - globals.pushEvent("cursor_data", cd); - /* - let ctx = cursorCanvas.getContext("2d"); - cursorCanvas.width = cd.width; - cursorCanvas.height = cd.height; - let imgData = new ImageData( - new Uint8ClampedArray(c), - cd.width, - cd.height - ); - ctx?.clearRect(0, 0, cd.width, cd.height); - ctx?.putImageData(imgData, 0, 0); - let url = cursorCanvas.toDataURL(); - const img = document.createElement("img"); - img.src = url; - this._cursors[cd.id] = img; - //cursorCanvas.width /= 2.; - //cursorCanvas.height /= 2.; - //ctx?.drawImage(img, cursorCanvas.width, cursorCanvas.height); - url = cursorCanvas.toDataURL(); - document.body.style.cursor = - "url(" + url + ")" + cd.hotx + " " + cd.hoty + ", default"; - console.log(document.body.style.cursor); - */ - } else if (msg?.cursor_id) { - globals.pushEvent("cursor_id", { id: msg?.cursor_id }); - } else if (msg?.cursor_position) { - globals.pushEvent("cursor_position", msg?.cursor_position); - } else if (msg?.misc) { - if (!this.handleMisc(msg?.misc)) break; - } else if (msg?.audio_frame) { - globals.playAudio(msg?.audio_frame.data); - } - } - } - - msgbox(type_: string, title: string, text: string) { - this._msgbox?.(type_, title, text); - } - - draw(frame: any) { - this._draw?.(frame); - globals.draw(frame); - } - - close() { - this._msgs = []; - clearInterval(this._interval); - this._ws?.close(); - this._videoDecoder?.close(); - } - - refresh() { - const misc = message.Misc.fromPartial({ refresh_video: true }); - this._ws?.sendMessage({ misc }); - } - - setMsgbox(callback: MsgboxCallback) { - this._msgbox = callback; - } - - setDraw(callback: DrawCallback) { - this._draw = callback; - } - - login(password: string | undefined = undefined) { - if (password) { - const salt = this._hash?.salt; - let p = hash([password, salt!]); - this._password = p; - const challenge = this._hash?.challenge; - p = hash([p, challenge!]); - this.msgbox("connecting", "Connecting...", "Logging in..."); - this._sendLoginMessage(p); - } else { - let p = this._password; - if (p) { - const challenge = this._hash?.challenge; - p = hash([p, challenge!]); - } - this._sendLoginMessage(p); - } - } - - async reconnect() { - this.close(); - await this.start(this._id); - } - - _sendLoginMessage(password: Uint8Array | undefined = undefined) { - const login_request = message.LoginRequest.fromPartial({ - username: this._id!, - my_id: "web", // to-do - my_name: "web", // to-do - password, - option: this.getOptionMessage(), - video_ack_required: true, - }); - this._ws?.sendMessage({ login_request }); - } - - getOptionMessage(): message.OptionMessage | undefined { - let n = 0; - const msg = message.OptionMessage.fromPartial({}); - const q = this.getImageQualityEnum(this.getImageQuality(), true); - const yes = message.OptionMessage_BoolOption.Yes; - if (q != undefined) { - msg.image_quality = q; - n += 1; - } - if (this._options["show-remote-cursor"]) { - msg.show_remote_cursor = yes; - n += 1; - } - if (this._options["lock-after-session-end"]) { - msg.lock_after_session_end = yes; - n += 1; - } - if (this._options["privacy-mode"]) { - msg.privacy_mode = yes; - n += 1; - } - if (this._options["disable-audio"]) { - msg.disable_audio = yes; - n += 1; - } - if (this._options["disable-clipboard"]) { - msg.disable_clipboard = yes; - n += 1; - } - return n > 0 ? msg : undefined; - } - - sendVideoReceived() { - const misc = message.Misc.fromPartial({ video_received: true }); - this._ws?.sendMessage({ misc }); - } - - handleVideoFrame(vf: message.VideoFrame) { - if (!this._firstFrame) { - this.msgbox("", "", ""); - this._firstFrame = true; - } - if (vf.vp9s) { - const dec = this._videoDecoder; - var tm = new Date().getTime(); - var i = 0; - const n = vf.vp9s?.frames.length; - vf.vp9s.frames.forEach((f) => { - dec.processFrame(f.data.slice(0).buffer, (ok: any) => { - i++; - if (i == n) this.sendVideoReceived(); - if (ok && dec.frameBuffer && n == i) { - this.draw(dec.frameBuffer); - const now = new Date().getTime(); - var elapsed = now - tm; - this._videoTestSpeed[1] += elapsed; - this._videoTestSpeed[0] += 1; - if (this._videoTestSpeed[0] >= 30) { - console.log( - "video decoder: " + - parseInt( - "" + this._videoTestSpeed[1] / this._videoTestSpeed[0] - ) - ); - this._videoTestSpeed = [0, 0]; - } - } - }); - }); - } - } - - handlePeerInfo(pi: message.PeerInfo) { - this._peerInfo = pi; - if (pi.displays.length == 0) { - this.msgbox("error", "Remote Error", "No Display"); - return; - } - this.msgbox("success", "Successful", "Connected, waiting for image..."); - globals.pushEvent("peer_info", pi); - const p = this.shouldAutoLogin(); - if (p) this.inputOsPassword(p); - const username = this.getOption("info")?.username; - if (username && !pi.username) pi.username = username; - this.setOption("info", pi); - if (this.getRemember()) { - if (this._password?.length) { - const p = this._password.toString(); - if (p != this.getOption("password")) { - this.setOption("password", p); - console.log("remember password of " + this._id); - } - } - } else { - this.setOption("password", undefined); - } - } - - shouldAutoLogin(): string { - const l = this.getOption("lock-after-session-end"); - const a = !!this.getOption("auto-login"); - const p = this.getOption("os-password"); - if (p && l && a) { - return p; - } - return ""; - } - - handleMisc(misc: message.Misc) { - if (misc.audio_format) { - globals.initAudio( - misc.audio_format.channels, - misc.audio_format.sample_rate - ); - } else if (misc.chat_message) { - globals.pushEvent("chat", { text: misc.chat_message.text }); - } else if (misc.permission_info) { - const p = misc.permission_info; - console.info("Change permission " + p.permission + " -> " + p.enabled); - let name; - switch (p.permission) { - case message.PermissionInfo_Permission.Keyboard: - name = "keyboard"; - break; - case message.PermissionInfo_Permission.Clipboard: - name = "clipboard"; - break; - case message.PermissionInfo_Permission.Audio: - name = "audio"; - break; - default: - return; - } - globals.pushEvent("permission", { [name]: p.enabled }); - } else if (misc.switch_display) { - this.loadVideoDecoder(); - globals.pushEvent("switch_display", misc.switch_display); - } else if (misc.close_reason) { - this.msgbox("error", "Connection Error", misc.close_reason); - this.close(); - return false; - } - return true; - } - - getRemember(): Boolean { - return this._options["remember"] || false; - } - - setRemember(v: Boolean) { - this.setOption("remember", v); - } - - getOption(name: string): any { - return this._options[name]; - } - - setOption(name: string, value: any) { - if (value == undefined) { - delete this._options[name]; - } else { - this._options[name] = value; - } - this._options["tm"] = new Date().getTime(); - const peers = globals.getPeers(); - peers[this._id] = this._options; - localStorage.setItem("peers", JSON.stringify(peers)); - } - - inputKey( - name: string, - down: boolean, - press: boolean, - alt: Boolean, - ctrl: Boolean, - shift: Boolean, - command: Boolean - ) { - const key_event = mapKey(name, globals.isDesktop()); - if (!key_event) return; - if (alt && (name == "VK_MENU" || name == "RAlt")) { - alt = false; - } - if (ctrl && (name == "VK_CONTROL" || name == "RControl")) { - ctrl = false; - } - if (shift && (name == "VK_SHIFT" || name == "RShift")) { - shift = false; - } - if (command && (name == "Meta" || name == "RWin")) { - command = false; - } - key_event.down = down; - key_event.press = press; - key_event.modifiers = this.getMod(alt, ctrl, shift, command); - this._ws?.sendMessage({ key_event }); - } - - ctrlAltDel() { - const key_event = message.KeyEvent.fromPartial({ down: true }); - if (this._peerInfo?.platform == "Windows") { - key_event.control_key = message.ControlKey.CtrlAltDel; - } else { - key_event.control_key = message.ControlKey.Delete; - key_event.modifiers = this.getMod(true, true, false, false); - } - this._ws?.sendMessage({ key_event }); - } - - inputString(seq: string) { - const key_event = message.KeyEvent.fromPartial({ seq }); - this._ws?.sendMessage({ key_event }); - } - - switchDisplay(display: number) { - const switch_display = message.SwitchDisplay.fromPartial({ display }); - const misc = message.Misc.fromPartial({ switch_display }); - this._ws?.sendMessage({ misc }); - } - - async inputOsPassword(seq: string) { - this.inputMouse(); - await sleep(50); - this.inputMouse(0, 3, 3); - await sleep(50); - this.inputMouse(1 | (1 << 3)); - this.inputMouse(2 | (1 << 3)); - await sleep(1200); - const key_event = message.KeyEvent.fromPartial({ press: true, seq }); - this._ws?.sendMessage({ key_event }); - } - - lockScreen() { - const key_event = message.KeyEvent.fromPartial({ - down: true, - control_key: message.ControlKey.LockScreen, - }); - this._ws?.sendMessage({ key_event }); - } - - getMod(alt: Boolean, ctrl: Boolean, shift: Boolean, command: Boolean) { - const mod: message.ControlKey[] = []; - if (alt) mod.push(message.ControlKey.Alt); - if (ctrl) mod.push(message.ControlKey.Control); - if (shift) mod.push(message.ControlKey.Shift); - if (command) mod.push(message.ControlKey.Meta); - return mod; - } - - inputMouse( - mask: number = 0, - x: number = 0, - y: number = 0, - alt: Boolean = false, - ctrl: Boolean = false, - shift: Boolean = false, - command: Boolean = false - ) { - const mouse_event = message.MouseEvent.fromPartial({ - mask, - x, - y, - modifiers: this.getMod(alt, ctrl, shift, command), - }); - this._ws?.sendMessage({ mouse_event }); - } - - toggleOption(name: string) { - const v = !this._options[name]; - const option = message.OptionMessage.fromPartial({}); - const v2 = v - ? message.OptionMessage_BoolOption.Yes - : message.OptionMessage_BoolOption.No; - switch (name) { - case "show-remote-cursor": - option.show_remote_cursor = v2; - break; - case "disable-audio": - option.disable_audio = v2; - break; - case "disable-clipboard": - option.disable_clipboard = v2; - break; - case "lock-after-session-end": - option.lock_after_session_end = v2; - break; - case "privacy-mode": - option.privacy_mode = v2; - break; - case "block-input": - option.block_input = message.OptionMessage_BoolOption.Yes; - break; - case "unblock-input": - option.block_input = message.OptionMessage_BoolOption.No; - break; - default: - return; - } - if (name.indexOf("block-input") < 0) this.setOption(name, v); - const misc = message.Misc.fromPartial({ option }); - this._ws?.sendMessage({ misc }); - } - - getImageQuality() { - return this.getOption("image-quality"); - } - - getImageQualityEnum( - value: string, - ignoreDefault: Boolean - ): message.ImageQuality | undefined { - switch (value) { - case "low": - return message.ImageQuality.Low; - case "best": - return message.ImageQuality.Best; - case "balanced": - return ignoreDefault ? undefined : message.ImageQuality.Balanced; - default: - return undefined; - } - } - - setImageQuality(value: string) { - this.setOption("image-quality", value); - const image_quality = this.getImageQualityEnum(value, false); - if (image_quality == undefined) return; - const option = message.OptionMessage.fromPartial({ image_quality }); - const misc = message.Misc.fromPartial({ option }); - this._ws?.sendMessage({ misc }); - } - - loadVideoDecoder() { - this._videoDecoder?.close(); - loadVp9((decoder: any) => { - this._videoDecoder = decoder; - console.log("vp9 loaded"); - console.log(decoder); - }); - } -} - -function testDelay() { - var nearest = ""; - HOSTS.forEach((host) => { - const now = new Date().getTime(); - new Websock(getrUriFromRs(host), true).open().then(() => { - console.log("latency of " + host + ": " + (new Date().getTime() - now)); - if (!nearest) { - HOST = host; - localStorage.setItem("rendezvous-server", host); - } - }); - }); -} - -testDelay(); - -function getDefaultUri(isRelay: Boolean = false): string { - const host = localStorage.getItem("custom-rendezvous-server"); - return getrUriFromRs(host || HOST, isRelay); -} - -function getrUriFromRs( - uri: string, - isRelay: Boolean = false, - roffset: number = 0 -): string { - if (uri.indexOf(":") > 0) { - const tmp = uri.split(":"); - const port = parseInt(tmp[1]); - uri = tmp[0] + ":" + (port + (isRelay ? roffset || 3 : 2)); - } else { - uri += ":" + (PORT + (isRelay ? 3 : 2)); - } - return SCHEMA + uri; -} - -function hash(datas: (string | Uint8Array)[]): Uint8Array { - const hasher = new sha256.Hash(); - datas.forEach((data) => { - if (typeof data == "string") { - data = new TextEncoder().encode(data); - } - return hasher.update(data); - }); - return hasher.digest(); -} diff --git a/flutter/web/v1/js/src/globals.js b/flutter/web/v1/js/src/globals.js deleted file mode 100644 index 953add18d01..00000000000 --- a/flutter/web/v1/js/src/globals.js +++ /dev/null @@ -1,383 +0,0 @@ -import Connection from "./connection"; -import _sodium from "libsodium-wrappers"; -import { CursorData } from "./message"; -import { loadVp9 } from "./codec"; -import { checkIfRetry, version } from "./gen_js_from_hbb"; -import { initZstd, translate } from "./common"; -import PCMPlayer from "pcm-player"; - -window.curConn = undefined; -window.isMobile = () => { - return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) - || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4)); -} - -export function isDesktop() { - return !isMobile(); -} - -export function msgbox(type, title, text) { - if (!type || (type == 'error' && !text)) return; - const text2 = text.toLowerCase(); - var hasRetry = checkIfRetry(type, title, text) ? 'true' : ''; - onGlobalEvent(JSON.stringify({ name: 'msgbox', type, title, text, hasRetry })); -} - -function jsonfyForDart(payload) { - var tmp = {}; - for (const [key, value] of Object.entries(payload)) { - if (!key) continue; - tmp[key] = value instanceof Uint8Array ? '[' + value.toString() + ']' : JSON.stringify(value); - } - return tmp; -} - -export function pushEvent(name, payload) { - payload = jsonfyForDart(payload); - payload.name = name; - onGlobalEvent(JSON.stringify(payload)); -} - -let yuvWorker; -let yuvCanvas; -let gl; -let pixels; -let flipPixels; -let oldSize; -if (YUVCanvas.WebGLFrameSink.isAvailable()) { - var canvas = document.createElement('canvas'); - yuvCanvas = YUVCanvas.attach(canvas, { webGL: true }); - gl = canvas.getContext("webgl"); -} else { - yuvWorker = new Worker("./yuv.js"); -} -let testSpeed = [0, 0]; - -export function draw(frame) { - if (yuvWorker) { - // frame's (y/u/v).bytes already detached, can not transferrable any more. - yuvWorker.postMessage(frame); - } else { - var tm0 = new Date().getTime(); - yuvCanvas.drawFrame(frame); - var width = canvas.width; - var height = canvas.height; - var size = width * height * 4; - if (size != oldSize) { - pixels = new Uint8Array(size); - flipPixels = new Uint8Array(size); - oldSize = size; - } - gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); - const row = width * 4; - const end = (height - 1) * row; - for (let i = 0; i < size; i += row) { - flipPixels.set(pixels.subarray(i, i + row), end - i); - } - onRgba(flipPixels); - testSpeed[1] += new Date().getTime() - tm0; - testSpeed[0] += 1; - if (testSpeed[0] > 30) { - console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0])); - testSpeed = [0, 0]; - } - } - /* - var testCanvas = document.getElementById("test-yuv-decoder-canvas"); - if (testCanvas && currentFrame) { - var ctx = testCanvas.getContext("2d"); - testCanvas.width = frame.format.displayWidth; - testCanvas.height = frame.format.displayHeight; - var img = ctx.createImageData(testCanvas.width, testCanvas.height); - img.data.set(currentFrame); - ctx.putImageData(img, 0, 0); - } - */ -} - -export function sendOffCanvas(c) { - let canvas = c.transferControlToOffscreen(); - yuvWorker.postMessage({ canvas }, [canvas]); -} - -export function setConn(conn) { - window.curConn = conn; -} - -export function getConn() { - return window.curConn; -} - -export async function startConn(id) { - setByName('remote_id', id); - await curConn.start(id); -} - -export function close() { - getConn()?.close(); - setConn(undefined); -} - -export function newConn() { - window.curConn?.close(); - const conn = new Connection(); - setConn(conn); - return conn; -} - -let sodium; -export async function verify(signed, pk) { - if (!sodium) { - await _sodium.ready; - sodium = _sodium; - } - if (typeof pk == 'string') { - pk = decodeBase64(pk); - } - return sodium.crypto_sign_open(signed, pk); -} - -export function decodeBase64(pk) { - return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL); -} - -export function genBoxKeyPair() { - const pair = sodium.crypto_box_keypair(); - const sk = pair.privateKey; - const pk = pair.publicKey; - return [sk, pk]; -} - -export function genSecretKey() { - return sodium.crypto_secretbox_keygen(); -} - -export function seal(unsigned, theirPk, ourSk) { - const nonce = Uint8Array.from(Array(24).fill(0)); - return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk); -} - -function makeOnce(value) { - var byteArray = Array(24).fill(0); - - for (var index = 0; index < byteArray.length && value > 0; index++) { - var byte = value & 0xff; - byteArray[index] = byte; - value = (value - byte) / 256; - } - - return Uint8Array.from(byteArray); -}; - -export function encrypt(unsigned, nonce, key) { - return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key); -} - -export function decrypt(signed, nonce, key) { - return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key); -} - -window.setByName = (name, value) => { - switch (name) { - case 'remote_id': - localStorage.setItem('remote-id', value); - break; - case 'connect': - newConn(); - startConn(value); - break; - case 'login': - value = JSON.parse(value); - curConn.setRemember(value.remember == 'true'); - curConn.login(value.password); - break; - case 'close': - close(); - break; - case 'refresh': - curConn.refresh(); - break; - case 'reconnect': - curConn.reconnect(); - break; - case 'toggle_option': - curConn.toggleOption(value); - break; - case 'image_quality': - curConn.setImageQuality(value); - break; - case 'lock_screen': - curConn.lockScreen(); - break; - case 'ctrl_alt_del': - curConn.ctrlAltDel(); - break; - case 'switch_display': - curConn.switchDisplay(value); - break; - case 'remove': - const peers = getPeers(); - delete peers[value]; - localStorage.setItem('peers', JSON.stringify(peers)); - break; - case 'input_key': - value = JSON.parse(value); - curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true'); - break; - case 'input_string': - curConn.inputString(value); - break; - case 'send_mouse': - let mask = 0; - value = JSON.parse(value); - switch (value.type) { - case 'down': - mask = 1; - break; - case 'up': - mask = 2; - break; - case 'wheel': - mask = 3; - break; - } - switch (value.buttons) { - case 'left': - mask |= 1 << 3; - break; - case 'right': - mask |= 2 << 3; - break; - case 'wheel': - mask |= 4 << 3; - } - curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true'); - break; - case 'option': - value = JSON.parse(value); - localStorage.setItem(value.name, value.value); - break; - case 'peer_option': - value = JSON.parse(value); - curConn.setOption(value.name, value.value); - break; - case 'input_os_password': - curConn.inputOsPassword(value); - break; - default: - break; - } -} - -window.getByName = (name, arg) => { - let v = _getByName(name, arg); - if (typeof v == 'string' || v instanceof String) return v; - if (v == undefined || v == null) return ''; - return JSON.stringify(v); -} - -function getPeersForDart() { - const peers = []; - for (const [id, value] of Object.entries(getPeers())) { - if (!id) continue; - const tm = value['tm']; - const info = value['info']; - if (!tm || !info) continue; - peers.push([tm, id, info]); - } - return peers.sort().reverse().map(x => x.slice(1)); -} - -function _getByName(name, arg) { - switch (name) { - case 'peers': - return getPeersForDart(); - case 'remote_id': - return localStorage.getItem('remote-id'); - case 'remember': - return curConn.getRemember(); - case 'toggle_option': - return curConn.getOption(arg) || false; - case 'option': - return localStorage.getItem(arg); - case 'image_quality': - return curConn.getImageQuality(); - case 'translate': - arg = JSON.parse(arg); - return translate(arg.locale, arg.text); - case 'peer_option': - return curConn.getOption(arg); - case 'test_if_valid_server': - break; - case 'version': - return version; - } - return ''; -} - -let opusWorker = new Worker("./libopus.js"); -let pcmPlayer; - -export function initAudio(channels, sampleRate) { - pcmPlayer = newAudioPlayer(channels, sampleRate); - opusWorker.postMessage({ channels, sampleRate }); -} - -export function playAudio(packet) { - opusWorker.postMessage(packet, [packet.buffer]); -} - -window.init = async () => { - if (yuvWorker) { - yuvWorker.onmessage = (e) => { - onRgba(e.data); - } - } - opusWorker.onmessage = (e) => { - pcmPlayer.feed(e.data); - } - loadVp9(() => { }); - await initZstd(); - console.log('init done'); -} - -export function getPeers() { - try { - return JSON.parse(localStorage.getItem('peers')) || {}; - } catch (e) { - return {}; - } -} - -function newAudioPlayer(channels, sampleRate) { - return new PCMPlayer({ - channels, - sampleRate, - flushingTime: 2000 - }); -} - -export function copyToClipboard(text) { - if (window.clipboardData && window.clipboardData.setData) { - // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible. - return window.clipboardData.setData("Text", text); - - } - else if (document.queryCommandSupported && document.queryCommandSupported("copy")) { - var textarea = document.createElement("textarea"); - textarea.textContent = text; - textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge. - document.body.appendChild(textarea); - textarea.select(); - try { - return document.execCommand("copy"); // Security exception may be thrown by some browsers. - } - catch (ex) { - console.warn("Copy to clipboard failed.", ex); - // return prompt("Copy to clipboard: Ctrl+C, Enter", text); - } - finally { - document.body.removeChild(textarea); - } - } -} \ No newline at end of file diff --git a/flutter/web/v1/js/src/main.ts b/flutter/web/v1/js/src/main.ts deleted file mode 100644 index 2be877f580f..00000000000 --- a/flutter/web/v1/js/src/main.ts +++ /dev/null @@ -1,2 +0,0 @@ -import "./globals"; -import "./ui"; \ No newline at end of file diff --git a/flutter/web/v1/js/src/style.css b/flutter/web/v1/js/src/style.css deleted file mode 100644 index 852de7aa2ae..00000000000 --- a/flutter/web/v1/js/src/style.css +++ /dev/null @@ -1,8 +0,0 @@ -#app { - font-family: Avenir, Helvetica, Arial, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-align: center; - color: #2c3e50; - margin-top: 60px; -} diff --git a/flutter/web/v1/js/src/ui.js b/flutter/web/v1/js/src/ui.js deleted file mode 100644 index 4463340228e..00000000000 --- a/flutter/web/v1/js/src/ui.js +++ /dev/null @@ -1,108 +0,0 @@ -import "./style.css"; -import "./connection"; -import * as globals from "./globals"; - -const app = document.querySelector('#app'); - -if (app) { - app.innerHTML = ` -
    - - - - -
    Host:
    Key:
    Id:
    - - - -`; - - let player; - window.init(); - - document.body.onload = () => { - const host = document.querySelector('#host'); - host.value = localStorage.getItem('custom-rendezvous-server'); - const id = document.querySelector('#id'); - id.value = localStorage.getItem('id'); - const key = document.querySelector('#key'); - key.value = localStorage.getItem('key'); - player = YUVCanvas.attach(document.getElementById('player')); - // globals.sendOffCanvas(document.getElementById('player')); - }; - - window.connect = () => { - const host = document.querySelector('#host'); - localStorage.setItem('custom-rendezvous-server', host.value); - const id = document.querySelector('#id'); - localStorage.setItem('id', id.value); - const key = document.querySelector('#key'); - localStorage.setItem('key', key.value); - const func = async () => { - const conn = globals.newConn(); - conn.setMsgbox(msgbox); - conn.setDraw((f) => { - /* - if (!(document.getElementById('player').width > 0)) { - document.getElementById('player').width = f.format.displayWidth; - document.getElementById('player').height = f.format.displayHeight; - } - */ - globals.draw(f); - player.drawFrame(f); - }); - document.querySelector('div#status').style.display = 'block'; - document.querySelector('div#connect').style.display = 'none'; - document.querySelector('div#text').innerHTML = 'Connecting ...'; - await conn.start(id.value); - }; - func(); - } - - function msgbox(type, title, text) { - if (!globals.getConn()) return; - if (type == 'input-password') { - document.querySelector('div#status').style.display = 'none'; - document.querySelector('div#password').style.display = 'block'; - } else if (!type) { - document.querySelector('div#canvas').style.display = 'block'; - document.querySelector('div#password').style.display = 'none'; - document.querySelector('div#status').style.display = 'none'; - } else if (type == 'error') { - document.querySelector('div#status').style.display = 'block'; - document.querySelector('div#canvas').style.display = 'none'; - document.querySelector('div#text').innerHTML = '
    ' + text + '
    '; - } else { - document.querySelector('div#password').style.display = 'none'; - document.querySelector('div#status').style.display = 'block'; - document.querySelector('div#text').innerHTML = '
    ' + text + '
    '; - } - } - - window.cancel = () => { - globals.close(); - document.querySelector('div#connect').style.display = 'block'; - document.querySelector('div#password').style.display = 'none'; - document.querySelector('div#status').style.display = 'none'; - document.querySelector('div#canvas').style.display = 'none'; - } - - window.confirm = () => { - const password = document.querySelector('input#password').value; - if (password) { - document.querySelector('div#password').style.display = 'none'; - globals.getConn().login(password); - } - } -} \ No newline at end of file diff --git a/flutter/web/v1/js/src/vite-env.d.ts b/flutter/web/v1/js/src/vite-env.d.ts deleted file mode 100644 index 151aa6856ff..00000000000 --- a/flutter/web/v1/js/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/flutter/web/v1/js/src/websock.ts b/flutter/web/v1/js/src/websock.ts deleted file mode 100644 index 6f05e6f6bd1..00000000000 --- a/flutter/web/v1/js/src/websock.ts +++ /dev/null @@ -1,183 +0,0 @@ -import * as message from "./message.js"; -import * as rendezvous from "./rendezvous.js"; -import * as globals from "./globals"; - -type Keys = "message" | "open" | "close" | "error"; - -export default class Websock { - _websocket: WebSocket; - _eventHandlers: { [key in Keys]: Function }; - _buf: (rendezvous.RendezvousMessage | message.Message)[]; - _status: any; - _latency: number; - _secretKey: [Uint8Array, number, number] | undefined; - _uri: string; - _isRendezvous: boolean; - - constructor(uri: string, isRendezvous: boolean = true) { - this._eventHandlers = { - message: (_: any) => {}, - open: () => {}, - close: () => {}, - error: () => {}, - }; - this._uri = uri; - this._status = ""; - this._buf = []; - this._websocket = new WebSocket(uri); - this._websocket.onmessage = this._recv_message.bind(this); - this._websocket.binaryType = "arraybuffer"; - this._latency = new Date().getTime(); - this._isRendezvous = isRendezvous; - } - - latency(): number { - return this._latency; - } - - setSecretKey(key: Uint8Array) { - this._secretKey = [key, 0, 0]; - } - - sendMessage(json: message.DeepPartial) { - let data = message.Message.encode( - message.Message.fromPartial(json) - ).finish(); - let k = this._secretKey; - if (k) { - k[1] += 1; - data = globals.encrypt(data, k[1], k[0]); - } - this._websocket.send(data); - } - - sendRendezvous(data: rendezvous.DeepPartial) { - this._websocket.send( - rendezvous.RendezvousMessage.encode( - rendezvous.RendezvousMessage.fromPartial(data) - ).finish() - ); - } - - parseMessage(data: Uint8Array) { - return message.Message.decode(data); - } - - parseRendezvous(data: Uint8Array) { - return rendezvous.RendezvousMessage.decode(data); - } - - // Event Handlers - off(evt: Keys) { - this._eventHandlers[evt] = () => {}; - } - - on(evt: Keys, handler: Function) { - this._eventHandlers[evt] = handler; - } - - async open(timeout: number = 12000): Promise { - return new Promise((resolve, reject) => { - setTimeout(() => { - if (this._status != "open") { - reject(this._status || "Timeout"); - } - }, timeout); - this._websocket.onopen = () => { - this._latency = new Date().getTime() - this._latency; - this._status = "open"; - console.debug(">> WebSock.onopen"); - if (this._websocket?.protocol) { - console.info( - "Server choose sub-protocol: " + this._websocket.protocol - ); - } - - this._eventHandlers.open(); - console.info("WebSock.onopen"); - resolve(this); - }; - this._websocket.onclose = (e) => { - if (this._status == "open") { - // e.code 1000 means that the connection was closed normally. - // - } - this._status = e; - console.error("WebSock.onclose: "); - console.error(e); - this._eventHandlers.close(e); - reject("Reset by the peer"); - }; - this._websocket.onerror = (e: any) => { - if (!this._status) { - reject("Failed to connect to " + (this._isRendezvous ? "rendezvous" : "relay") + " server"); - return; - } - this._status = e; - console.error("WebSock.onerror: ") - console.error(e); - this._eventHandlers.error(e); - }; - }); - } - - async next( - timeout = 12000 - ): Promise { - const func = ( - resolve: (value: rendezvous.RendezvousMessage | message.Message) => void, - reject: (reason: any) => void, - tm0: number - ) => { - if (this._buf.length) { - resolve(this._buf[0]); - this._buf.splice(0, 1); - } else { - if (this._status != "open") { - reject(this._status); - return; - } - if (new Date().getTime() > tm0 + timeout) { - reject("Timeout"); - } else { - setTimeout(() => func(resolve, reject, tm0), 1); - } - } - }; - return new Promise((resolve, reject) => { - func(resolve, reject, new Date().getTime()); - }); - } - - close() { - this._status = ""; - if (this._websocket) { - if ( - this._websocket.readyState === WebSocket.OPEN || - this._websocket.readyState === WebSocket.CONNECTING - ) { - console.info("Closing WebSocket connection"); - this._websocket.close(); - } - - this._websocket.onmessage = () => {}; - } - } - - _recv_message(e: any) { - if (e.data instanceof window.ArrayBuffer) { - let bytes = new Uint8Array(e.data); - const k = this._secretKey; - if (k) { - k[2] += 1; - bytes = globals.decrypt(bytes, k[2], k[0]); - } - this._buf.push( - this._isRendezvous - ? this.parseRendezvous(bytes) - : this.parseMessage(bytes) - ); - } - this._eventHandlers.message(e.data); - } -} diff --git a/flutter/web/v1/js/ts_proto.py b/flutter/web/v1/js/ts_proto.py deleted file mode 100755 index 62a73fe7c12..00000000000 --- a/flutter/web/v1/js/ts_proto.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python - -import os - -path = os.path.abspath(os.path.join(os.getcwd(), '..', '..', '..', 'libs', 'hbb_common', 'protos')) - -if os.name == 'nt': - cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=protoc-gen-ts_proto=.\node_modules\.bin\protoc-gen-ts_proto.cmd -I "%s" --ts_proto_out=./src/ rendezvous.proto'%path - print(cmd) - os.system(cmd) - cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=protoc-gen-ts_proto=.\node_modules\.bin\protoc-gen-ts_proto.cmd -I "%s" --ts_proto_out=./src/ message.proto'%path - print(cmd) - os.system(cmd) -else: - cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=./node_modules/.bin/protoc-gen-ts_proto -I "%s" --ts_proto_out=./src/ rendezvous.proto'%path - print(cmd) - os.system(cmd) - cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=./node_modules/.bin/protoc-gen-ts_proto -I "%s" --ts_proto_out=./src/ message.proto'%path - print(cmd) - os.system(cmd) diff --git a/flutter/web/v1/js/tsconfig.json b/flutter/web/v1/js/tsconfig.json deleted file mode 100644 index ca949de6ad2..00000000000 --- a/flutter/web/v1/js/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "module": "ESNext", - "allowJs": true, - "lib": [ - "ESNext", - "DOM" - ], - "moduleResolution": "Node", - "strict": true, - "sourceMap": true, - "resolveJsonModule": true, - "esModuleInterop": true, - "noEmit": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true - }, - "include": [ - "./src" - ] -} \ No newline at end of file diff --git a/flutter/web/v1/js/vite.config.js b/flutter/web/v1/js/vite.config.js deleted file mode 100644 index 22c51fa54a0..00000000000 --- a/flutter/web/v1/js/vite.config.js +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from 'vite'; - -export default defineConfig({ - build: { - manifest: false, - rollupOptions: { - output: { - entryFileNames: `[name].js`, - chunkFileNames: `[name].js`, - assetFileNames: `[name].[ext]`, - } - } - }, -}) \ No newline at end of file diff --git a/flutter/web/v1/js/yarn.lock b/flutter/web/v1/js/yarn.lock deleted file mode 100644 index d5566487944..00000000000 --- a/flutter/web/v1/js/yarn.lock +++ /dev/null @@ -1,1494 +0,0 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 6 - cacheKey: 8 - -"@gar/promisify@npm:^1.1.3": - version: 1.1.3 - resolution: "@gar/promisify@npm:1.1.3" - checksum: 4059f790e2d07bf3c3ff3e0fec0daa8144fe35c1f6e0111c9921bd32106adaa97a4ab096ad7dab1e28ee6a9060083c4d1a4ada42a7f5f3f7a96b8812e2b757c1 - languageName: node - linkType: hard - -"@npmcli/fs@npm:^2.1.0": - version: 2.1.0 - resolution: "@npmcli/fs@npm:2.1.0" - dependencies: - "@gar/promisify": ^1.1.3 - semver: ^7.3.5 - checksum: 6ec6d678af6da49f9dac50cd882d7f661934dd278972ffbaacde40d9eaa2871292d634000a0cca9510f6fc29855fbd4af433e1adbff90a524ec3eaf140f1219b - languageName: node - linkType: hard - -"@npmcli/move-file@npm:^2.0.0": - version: 2.0.0 - resolution: "@npmcli/move-file@npm:2.0.0" - dependencies: - mkdirp: ^1.0.4 - rimraf: ^3.0.2 - checksum: 1388777b507b0c592d53f41b9d182e1a8de7763bc625fc07999b8edbc22325f074e5b3ec90af79c89d6987fdb2325bc66d59f483258543c14a43661621f841b0 - languageName: node - linkType: hard - -"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/aspromise@npm:1.1.2" - checksum: 011fe7ef0826b0fd1a95935a033a3c0fd08483903e1aa8f8b4e0704e3233406abb9ee25350ec0c20bbecb2aad8da0dcea58b392bbd77d6690736f02c143865d2 - languageName: node - linkType: hard - -"@protobufjs/base64@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/base64@npm:1.1.2" - checksum: 67173ac34de1e242c55da52c2f5bdc65505d82453893f9b51dc74af9fe4c065cf4a657a4538e91b0d4a1a1e0a0642215e31894c31650ff6e3831471061e1ee9e - languageName: node - linkType: hard - -"@protobufjs/codegen@npm:^2.0.4": - version: 2.0.4 - resolution: "@protobufjs/codegen@npm:2.0.4" - checksum: 59240c850b1d3d0b56d8f8098dd04787dcaec5c5bd8de186fa548de86b86076e1c50e80144b90335e705a044edf5bc8b0998548474c2a10a98c7e004a1547e4b - languageName: node - linkType: hard - -"@protobufjs/eventemitter@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/eventemitter@npm:1.1.0" - checksum: 0369163a3d226851682f855f81413cbf166cd98f131edb94a0f67f79e75342d86e89df9d7a1df08ac28be2bc77e0a7f0200526bb6c2a407abbfee1f0262d5fd7 - languageName: node - linkType: hard - -"@protobufjs/fetch@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/fetch@npm:1.1.0" - dependencies: - "@protobufjs/aspromise": ^1.1.1 - "@protobufjs/inquire": ^1.1.0 - checksum: 3fce7e09eb3f1171dd55a192066450f65324fd5f7cc01a431df01bb00d0a895e6bfb5b0c5561ce157ee1d886349c90703d10a4e11a1a256418ff591b969b3477 - languageName: node - linkType: hard - -"@protobufjs/float@npm:^1.0.2": - version: 1.0.2 - resolution: "@protobufjs/float@npm:1.0.2" - checksum: 5781e1241270b8bd1591d324ca9e3a3128d2f768077a446187a049e36505e91bc4156ed5ac3159c3ce3d2ba3743dbc757b051b2d723eea9cd367bfd54ab29b2f - languageName: node - linkType: hard - -"@protobufjs/inquire@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/inquire@npm:1.1.0" - checksum: ca06f02eaf65ca36fb7498fc3492b7fc087bfcc85c702bac5b86fad34b692bdce4990e0ef444c1e2aea8c034227bd1f0484be02810d5d7e931c55445555646f4 - languageName: node - linkType: hard - -"@protobufjs/path@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/path@npm:1.1.2" - checksum: 856eeb532b16a7aac071cacde5c5620df800db4c80cee6dbc56380524736205aae21e5ae47739114bf669ab5e8ba0e767a282ad894f3b5e124197cb9224445ee - languageName: node - linkType: hard - -"@protobufjs/pool@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/pool@npm:1.1.0" - checksum: d6a34fbbd24f729e2a10ee915b74e1d77d52214de626b921b2d77288bd8f2386808da2315080f2905761527cceffe7ec34c7647bd21a5ae41a25e8212ff79451 - languageName: node - linkType: hard - -"@protobufjs/utf8@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/utf8@npm:1.1.0" - checksum: f9bf3163d13aaa3b6f5e6fbf37a116e094ea021c0e1f2a7ccd0e12a29e2ce08dafba4e8b36e13f8ed7397e1591610ce880ed1289af4d66cf4ace8a36a9557278 - languageName: node - linkType: hard - -"@tootallnate/once@npm:2": - version: 2.0.0 - resolution: "@tootallnate/once@npm:2.0.0" - checksum: ad87447820dd3f24825d2d947ebc03072b20a42bfc96cbafec16bff8bbda6c1a81fcb0be56d5b21968560c5359a0af4038a68ba150c3e1694fe4c109a063bed8 - languageName: node - linkType: hard - -"@types/long@npm:^4.0.1": - version: 4.0.1 - resolution: "@types/long@npm:4.0.1" - checksum: ff9653c33f5000d0f131fd98a950a0343e2e33107dd067a97ac4a3b9678e1a2e39ea44772ad920f54ef6e8f107f76bc92c2584ba905a0dc4253282a4101166d0 - languageName: node - linkType: hard - -"@types/node@npm:>=13.7.0": - version: 17.0.8 - resolution: "@types/node@npm:17.0.8" - checksum: f4cadeb9e602027520abc88c77142697e33cf6ac98bb02f8b595a398603cbd33df1f94d01c055c9f13cde0c8eaafc5e396ca72645458d42b4318b845bc7f1d0f - languageName: node - linkType: hard - -"@types/object-hash@npm:^1.3.0": - version: 1.3.4 - resolution: "@types/object-hash@npm:1.3.4" - checksum: fe4aa041427f3c69cbcf63434af6e788329b40d7208b30aa845cfc1aa6bf9b0c11b23fa33a567d85ba7f2574a95c3b4a227f4b9b9b55da1eaea68ab94b4058d9 - languageName: node - linkType: hard - -"abbrev@npm:1": - version: 1.1.1 - resolution: "abbrev@npm:1.1.1" - checksum: a4a97ec07d7ea112c517036882b2ac22f3109b7b19077dc656316d07d308438aac28e4d9746dc4d84bf6b1e75b4a7b0a5f3cb30592419f128ca9a8cee3bcfa17 - languageName: node - linkType: hard - -"agent-base@npm:6, agent-base@npm:^6.0.2": - version: 6.0.2 - resolution: "agent-base@npm:6.0.2" - dependencies: - debug: 4 - checksum: f52b6872cc96fd5f622071b71ef200e01c7c4c454ee68bc9accca90c98cfb39f2810e3e9aa330435835eedc8c23f4f8a15267f67c6e245d2b33757575bdac49d - languageName: node - linkType: hard - -"agentkeepalive@npm:^4.2.1": - version: 4.2.1 - resolution: "agentkeepalive@npm:4.2.1" - dependencies: - debug: ^4.1.0 - depd: ^1.1.2 - humanize-ms: ^1.2.1 - checksum: 39cb49ed8cf217fd6da058a92828a0a84e0b74c35550f82ee0a10e1ee403c4b78ade7948be2279b188b7a7303f5d396ea2738b134731e464bf28de00a4f72a18 - languageName: node - linkType: hard - -"aggregate-error@npm:^3.0.0": - version: 3.1.0 - resolution: "aggregate-error@npm:3.1.0" - dependencies: - clean-stack: ^2.0.0 - indent-string: ^4.0.0 - checksum: 1101a33f21baa27a2fa8e04b698271e64616b886795fd43c31068c07533c7b3facfcaf4e9e0cab3624bd88f729a592f1c901a1a229c9e490eafce411a8644b79 - languageName: node - linkType: hard - -"ansi-regex@npm:^5.0.1": - version: 5.0.1 - resolution: "ansi-regex@npm:5.0.1" - checksum: 2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b - languageName: node - linkType: hard - -"aproba@npm:^1.0.3 || ^2.0.0": - version: 2.0.0 - resolution: "aproba@npm:2.0.0" - checksum: 5615cadcfb45289eea63f8afd064ab656006361020e1735112e346593856f87435e02d8dcc7ff0d11928bc7d425f27bc7c2a84f6c0b35ab0ff659c814c138a24 - languageName: node - linkType: hard - -"are-we-there-yet@npm:^3.0.0": - version: 3.0.0 - resolution: "are-we-there-yet@npm:3.0.0" - dependencies: - delegates: ^1.0.0 - readable-stream: ^3.6.0 - checksum: 348edfdd931b0b50868b55402c01c3f64df1d4c229ab6f063539a5025fd6c5f5bb8a0cab409bbed8d75d34762d22aa91b7c20b4204eb8177063158d9ba792981 - languageName: node - linkType: hard - -"balanced-match@npm:^1.0.0": - version: 1.0.2 - resolution: "balanced-match@npm:1.0.2" - checksum: 9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 - languageName: node - linkType: hard - -"brace-expansion@npm:^1.1.7": - version: 1.1.11 - resolution: "brace-expansion@npm:1.1.11" - dependencies: - balanced-match: ^1.0.0 - concat-map: 0.0.1 - checksum: faf34a7bb0c3fcf4b59c7808bc5d2a96a40988addf2e7e09dfbb67a2251800e0d14cd2bfc1aa79174f2f5095c54ff27f46fb1289fe2d77dac755b5eb3434cc07 - languageName: node - linkType: hard - -"brace-expansion@npm:^2.0.1": - version: 2.0.1 - resolution: "brace-expansion@npm:2.0.1" - dependencies: - balanced-match: ^1.0.0 - checksum: a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 - languageName: node - linkType: hard - -"cacache@npm:^16.0.2": - version: 16.0.7 - resolution: "cacache@npm:16.0.7" - dependencies: - "@npmcli/fs": ^2.1.0 - "@npmcli/move-file": ^2.0.0 - chownr: ^2.0.0 - fs-minipass: ^2.1.0 - glob: ^8.0.1 - infer-owner: ^1.0.4 - lru-cache: ^7.7.1 - minipass: ^3.1.6 - minipass-collect: ^1.0.2 - minipass-flush: ^1.0.5 - minipass-pipeline: ^1.2.4 - mkdirp: ^1.0.4 - p-map: ^4.0.0 - promise-inflight: ^1.0.1 - rimraf: ^3.0.2 - ssri: ^9.0.0 - tar: ^6.1.11 - unique-filename: ^1.1.1 - checksum: 2155b099b7e0f0369fb1155ca4673532ca7efe2ebdbec63acca8743580b8446b5d4fd7184626b1cb059001af77b981cdc67035c7855544d365d4f048eafca2ca - languageName: node - linkType: hard - -"chownr@npm:^2.0.0": - version: 2.0.0 - resolution: "chownr@npm:2.0.0" - checksum: c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f - languageName: node - linkType: hard - -"clean-stack@npm:^2.0.0": - version: 2.2.0 - resolution: "clean-stack@npm:2.2.0" - checksum: 2ac8cd2b2f5ec986a3c743935ec85b07bc174d5421a5efc8017e1f146a1cf5f781ae962618f416352103b32c9cd7e203276e8c28241bbe946160cab16149fb68 - languageName: node - linkType: hard - -"color-support@npm:^1.1.3": - version: 1.1.3 - resolution: "color-support@npm:1.1.3" - bin: - color-support: bin.js - checksum: 9b7356817670b9a13a26ca5af1c21615463b500783b739b7634a0c2047c16cef4b2865d7576875c31c3cddf9dd621fa19285e628f20198b233a5cfdda6d0793b - languageName: node - linkType: hard - -"concat-map@npm:0.0.1": - version: 0.0.1 - resolution: "concat-map@npm:0.0.1" - checksum: 902a9f5d8967a3e2faf138d5cb784b9979bad2e6db5357c5b21c568df4ebe62bcb15108af1b2253744844eb964fc023fbd9afbbbb6ddd0bcc204c6fb5b7bf3af - languageName: node - linkType: hard - -"console-control-strings@npm:^1.1.0": - version: 1.1.0 - resolution: "console-control-strings@npm:1.1.0" - checksum: 8755d76787f94e6cf79ce4666f0c5519906d7f5b02d4b884cf41e11dcd759ed69c57da0670afd9236d229a46e0f9cf519db0cd829c6dca820bb5a5c3def584ed - languageName: node - linkType: hard - -"dataloader@npm:^1.4.0": - version: 1.4.0 - resolution: "dataloader@npm:1.4.0" - checksum: e2c93d43afde68980efc0cd9ff48e9851116e27a9687f863e02b56d46f7e7868cc762cd6dcbaf4197e1ca850a03651510c165c2ae24b8e9843fd894002ad0e20 - languageName: node - linkType: hard - -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.3": - version: 4.3.4 - resolution: "debug@npm:4.3.4" - dependencies: - ms: 2.1.2 - peerDependenciesMeta: - supports-color: - optional: true - checksum: 3dbad3f94ea64f34431a9cbf0bafb61853eda57bff2880036153438f50fb5a84f27683ba0d8e5426bf41a8c6ff03879488120cf5b3a761e77953169c0600a708 - languageName: node - linkType: hard - -"delegates@npm:^1.0.0": - version: 1.0.0 - resolution: "delegates@npm:1.0.0" - checksum: a51744d9b53c164ba9c0492471a1a2ffa0b6727451bdc89e31627fdf4adda9d51277cfcbfb20f0a6f08ccb3c436f341df3e92631a3440226d93a8971724771fd - languageName: node - linkType: hard - -"depd@npm:^1.1.2": - version: 1.1.2 - resolution: "depd@npm:1.1.2" - checksum: 6b406620d269619852885ce15965272b829df6f409724415e0002c8632ab6a8c0a08ec1f0bd2add05dc7bd7507606f7e2cc034fa24224ab829580040b835ecd9 - languageName: node - linkType: hard - -"emoji-regex@npm:^8.0.0": - version: 8.0.0 - resolution: "emoji-regex@npm:8.0.0" - checksum: d4c5c39d5a9868b5fa152f00cada8a936868fd3367f33f71be515ecee4c803132d11b31a6222b2571b1e5f7e13890156a94880345594d0ce7e3c9895f560f192 - languageName: node - linkType: hard - -"encoding@npm:^0.1.13": - version: 0.1.13 - resolution: "encoding@npm:0.1.13" - dependencies: - iconv-lite: ^0.6.2 - checksum: bb98632f8ffa823996e508ce6a58ffcf5856330fde839ae42c9e1f436cc3b5cc651d4aeae72222916545428e54fd0f6aa8862fd8d25bdbcc4589f1e3f3715e7f - languageName: node - linkType: hard - -"env-paths@npm:^2.2.0": - version: 2.2.1 - resolution: "env-paths@npm:2.2.1" - checksum: 65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e - languageName: node - linkType: hard - -"err-code@npm:^2.0.2": - version: 2.0.3 - resolution: "err-code@npm:2.0.3" - checksum: 8b7b1be20d2de12d2255c0bc2ca638b7af5171142693299416e6a9339bd7d88fc8d7707d913d78e0993176005405a236b066b45666b27b797252c771156ace54 - languageName: node - linkType: hard - -"esbuild-android-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-android-arm64@npm:0.13.15" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-darwin-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-darwin-64@npm:0.13.15" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"esbuild-darwin-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-darwin-arm64@npm:0.13.15" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-freebsd-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-freebsd-64@npm:0.13.15" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"esbuild-freebsd-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-freebsd-arm64@npm:0.13.15" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-linux-32@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-32@npm:0.13.15" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - -"esbuild-linux-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-64@npm:0.13.15" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - -"esbuild-linux-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-arm64@npm:0.13.15" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-linux-arm@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-arm@npm:0.13.15" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"esbuild-linux-mips64le@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-mips64le@npm:0.13.15" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - -"esbuild-linux-ppc64le@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-ppc64le@npm:0.13.15" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - -"esbuild-netbsd-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-netbsd-64@npm:0.13.15" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - -"esbuild-openbsd-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-openbsd-64@npm:0.13.15" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - -"esbuild-sunos-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-sunos-64@npm:0.13.15" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - -"esbuild-windows-32@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-windows-32@npm:0.13.15" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"esbuild-windows-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-windows-64@npm:0.13.15" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"esbuild-windows-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-windows-arm64@npm:0.13.15" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"esbuild@npm:^0.13.12": - version: 0.13.15 - resolution: "esbuild@npm:0.13.15" - dependencies: - esbuild-android-arm64: 0.13.15 - esbuild-darwin-64: 0.13.15 - esbuild-darwin-arm64: 0.13.15 - esbuild-freebsd-64: 0.13.15 - esbuild-freebsd-arm64: 0.13.15 - esbuild-linux-32: 0.13.15 - esbuild-linux-64: 0.13.15 - esbuild-linux-arm: 0.13.15 - esbuild-linux-arm64: 0.13.15 - esbuild-linux-mips64le: 0.13.15 - esbuild-linux-ppc64le: 0.13.15 - esbuild-netbsd-64: 0.13.15 - esbuild-openbsd-64: 0.13.15 - esbuild-sunos-64: 0.13.15 - esbuild-windows-32: 0.13.15 - esbuild-windows-64: 0.13.15 - esbuild-windows-arm64: 0.13.15 - dependenciesMeta: - esbuild-android-arm64: - optional: true - esbuild-darwin-64: - optional: true - esbuild-darwin-arm64: - optional: true - esbuild-freebsd-64: - optional: true - esbuild-freebsd-arm64: - optional: true - esbuild-linux-32: - optional: true - esbuild-linux-64: - optional: true - esbuild-linux-arm: - optional: true - esbuild-linux-arm64: - optional: true - esbuild-linux-mips64le: - optional: true - esbuild-linux-ppc64le: - optional: true - esbuild-netbsd-64: - optional: true - esbuild-openbsd-64: - optional: true - esbuild-sunos-64: - optional: true - esbuild-windows-32: - optional: true - esbuild-windows-64: - optional: true - esbuild-windows-arm64: - optional: true - bin: - esbuild: bin/esbuild - checksum: d5fac8f28a6328592e45f9d49a7e98420cf2c2a3ff5a753bbf011ab79bcb5c062209ef862d3ae0875d8f2a50d40c112b0224e8b419a7cbffc6e2b02cee11ef7e - languageName: node - linkType: hard - -"fast-sha256@npm:^1.3.0": - version: 1.3.0 - resolution: "fast-sha256@npm:1.3.0" - checksum: 2b0bea7d3a9955e67abd2d3fbef4ce57f7dbb75708fc206d14973bd1d97aaf35b5c0a59c1d65be6f755df43d73b7657b9eac4fb3c2d58e6849966db1ef1fa186 - languageName: node - linkType: hard - -"fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": - version: 2.1.0 - resolution: "fs-minipass@npm:2.1.0" - dependencies: - minipass: ^3.0.0 - checksum: 1b8d128dae2ac6cc94230cc5ead341ba3e0efaef82dab46a33d171c044caaa6ca001364178d42069b2809c35a1c3c35079a32107c770e9ffab3901b59af8c8b1 - languageName: node - linkType: hard - -"fs.realpath@npm:^1.0.0": - version: 1.0.0 - resolution: "fs.realpath@npm:1.0.0" - checksum: 99ddea01a7e75aa276c250a04eedeffe5662bce66c65c07164ad6264f9de18fb21be9433ead460e54cff20e31721c811f4fb5d70591799df5f85dce6d6746fd0 - languageName: node - linkType: hard - -"fsevents@npm:~2.3.2": - version: 2.3.2 - resolution: "fsevents@npm:2.3.2" - dependencies: - node-gyp: latest - checksum: 97ade64e75091afee5265e6956cb72ba34db7819b4c3e94c431d4be2b19b8bb7a2d4116da417950c3425f17c8fe693d25e20212cac583ac1521ad066b77ae31f - conditions: os=darwin - languageName: node - linkType: hard - -"fsevents@patch:fsevents@~2.3.2#~builtin": - version: 2.3.2 - resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=18f3a7" - dependencies: - node-gyp: latest - conditions: os=darwin - languageName: node - linkType: hard - -"function-bind@npm:^1.1.1": - version: 1.1.1 - resolution: "function-bind@npm:1.1.1" - checksum: b32fbaebb3f8ec4969f033073b43f5c8befbb58f1a79e12f1d7490358150359ebd92f49e72ff0144f65f2c48ea2a605bff2d07965f548f6474fd8efd95bf361a - languageName: node - linkType: hard - -"gauge@npm:^4.0.3": - version: 4.0.4 - resolution: "gauge@npm:4.0.4" - dependencies: - aproba: ^1.0.3 || ^2.0.0 - color-support: ^1.1.3 - console-control-strings: ^1.1.0 - has-unicode: ^2.0.1 - signal-exit: ^3.0.7 - string-width: ^4.2.3 - strip-ansi: ^6.0.1 - wide-align: ^1.1.5 - checksum: 788b6bfe52f1dd8e263cda800c26ac0ca2ff6de0b6eee2fe0d9e3abf15e149b651bd27bf5226be10e6e3edb5c4e5d5985a5a1a98137e7a892f75eff76467ad2d - languageName: node - linkType: hard - -"glob@npm:^7.1.3, glob@npm:^7.1.4": - version: 7.2.3 - resolution: "glob@npm:7.2.3" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^3.1.1 - once: ^1.3.0 - path-is-absolute: ^1.0.0 - checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 - languageName: node - linkType: hard - -"glob@npm:^8.0.1": - version: 8.0.3 - resolution: "glob@npm:8.0.3" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^5.0.1 - once: ^1.3.0 - checksum: 50bcdea19d8e79d8de5f460b1939ffc2b3299eac28deb502093fdca22a78efebc03e66bf54f0abc3d3d07d8134d19a32850288b7440d77e072aa55f9d33b18c5 - languageName: node - linkType: hard - -"graceful-fs@npm:^4.2.6": - version: 4.2.10 - resolution: "graceful-fs@npm:4.2.10" - checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da - languageName: node - linkType: hard - -"has-unicode@npm:^2.0.1": - version: 2.0.1 - resolution: "has-unicode@npm:2.0.1" - checksum: 1eab07a7436512db0be40a710b29b5dc21fa04880b7f63c9980b706683127e3c1b57cb80ea96d47991bdae2dfe479604f6a1ba410106ee1046a41d1bd0814400 - languageName: node - linkType: hard - -"has@npm:^1.0.3": - version: 1.0.3 - resolution: "has@npm:1.0.3" - dependencies: - function-bind: ^1.1.1 - checksum: b9ad53d53be4af90ce5d1c38331e712522417d017d5ef1ebd0507e07c2fbad8686fffb8e12ddecd4c39ca9b9b47431afbb975b8abf7f3c3b82c98e9aad052792 - languageName: node - linkType: hard - -"http-cache-semantics@npm:^4.1.0": - version: 4.1.0 - resolution: "http-cache-semantics@npm:4.1.0" - checksum: 974de94a81c5474be07f269f9fd8383e92ebb5a448208223bfb39e172a9dbc26feff250192ecc23b9593b3f92098e010406b0f24bd4d588d631f80214648ed42 - languageName: node - linkType: hard - -"http-proxy-agent@npm:^5.0.0": - version: 5.0.0 - resolution: "http-proxy-agent@npm:5.0.0" - dependencies: - "@tootallnate/once": 2 - agent-base: 6 - debug: 4 - checksum: e2ee1ff1656a131953839b2a19cd1f3a52d97c25ba87bd2559af6ae87114abf60971e498021f9b73f9fd78aea8876d1fb0d4656aac8a03c6caa9fc175f22b786 - languageName: node - linkType: hard - -"https-proxy-agent@npm:^5.0.0": - version: 5.0.1 - resolution: "https-proxy-agent@npm:5.0.1" - dependencies: - agent-base: 6 - debug: 4 - checksum: 571fccdf38184f05943e12d37d6ce38197becdd69e58d03f43637f7fa1269cf303a7d228aa27e5b27bbd3af8f09fd938e1c91dcfefff2df7ba77c20ed8dfc765 - languageName: node - linkType: hard - -"humanize-ms@npm:^1.2.1": - version: 1.2.1 - resolution: "humanize-ms@npm:1.2.1" - dependencies: - ms: ^2.0.0 - checksum: 9c7a74a2827f9294c009266c82031030eae811ca87b0da3dceb8d6071b9bde22c9f3daef0469c3c533cc67a97d8a167cd9fc0389350e5f415f61a79b171ded16 - languageName: node - linkType: hard - -"iconv-lite@npm:^0.6.2": - version: 0.6.3 - resolution: "iconv-lite@npm:0.6.3" - dependencies: - safer-buffer: ">= 2.1.2 < 3.0.0" - checksum: 3f60d47a5c8fc3313317edfd29a00a692cc87a19cac0159e2ce711d0ebc9019064108323b5e493625e25594f11c6236647d8e256fbe7a58f4a3b33b89e6d30bf - languageName: node - linkType: hard - -"imurmurhash@npm:^0.1.4": - version: 0.1.4 - resolution: "imurmurhash@npm:0.1.4" - checksum: 7cae75c8cd9a50f57dadd77482359f659eaebac0319dd9368bcd1714f55e65badd6929ca58569da2b6494ef13fdd5598cd700b1eba23f8b79c5f19d195a3ecf7 - languageName: node - linkType: hard - -"indent-string@npm:^4.0.0": - version: 4.0.0 - resolution: "indent-string@npm:4.0.0" - checksum: 824cfb9929d031dabf059bebfe08cf3137365e112019086ed3dcff6a0a7b698cb80cf67ccccde0e25b9e2d7527aa6cc1fed1ac490c752162496caba3e6699612 - languageName: node - linkType: hard - -"infer-owner@npm:^1.0.4": - version: 1.0.4 - resolution: "infer-owner@npm:1.0.4" - checksum: 181e732764e4a0611576466b4b87dac338972b839920b2a8cde43642e4ed6bd54dc1fb0b40874728f2a2df9a1b097b8ff83b56d5f8f8e3927f837fdcb47d8a89 - languageName: node - linkType: hard - -"inflight@npm:^1.0.4": - version: 1.0.6 - resolution: "inflight@npm:1.0.6" - dependencies: - once: ^1.3.0 - wrappy: 1 - checksum: f4f76aa072ce19fae87ce1ef7d221e709afb59d445e05d47fba710e85470923a75de35bfae47da6de1b18afc3ce83d70facf44cfb0aff89f0a3f45c0a0244dfd - languageName: node - linkType: hard - -"inherits@npm:2, inherits@npm:^2.0.3": - version: 2.0.4 - resolution: "inherits@npm:2.0.4" - checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 - languageName: node - linkType: hard - -"ip@npm:^1.1.5": - version: 1.1.8 - resolution: "ip@npm:1.1.8" - checksum: a2ade53eb339fb0cbe9e69a44caab10d6e3784662285eb5d2677117ee4facc33a64679051c35e0dfdb1a3983a51ce2f5d2cb36446d52e10d01881789b76e28fb - languageName: node - linkType: hard - -"is-core-module@npm:^2.8.0": - version: 2.8.1 - resolution: "is-core-module@npm:2.8.1" - dependencies: - has: ^1.0.3 - checksum: 418b7bc10768a73c41c7ef497e293719604007f88934a6ffc5f7c78702791b8528102fb4c9e56d006d69361549b3d9519440214a74aefc7e0b79e5e4411d377f - languageName: node - linkType: hard - -"is-fullwidth-code-point@npm:^3.0.0": - version: 3.0.0 - resolution: "is-fullwidth-code-point@npm:3.0.0" - checksum: 44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348 - languageName: node - linkType: hard - -"is-lambda@npm:^1.0.1": - version: 1.0.1 - resolution: "is-lambda@npm:1.0.1" - checksum: 93a32f01940220532e5948538699ad610d5924ac86093fcee83022252b363eb0cc99ba53ab084a04e4fb62bf7b5731f55496257a4c38adf87af9c4d352c71c35 - languageName: node - linkType: hard - -"isexe@npm:^2.0.0": - version: 2.0.0 - resolution: "isexe@npm:2.0.0" - checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62 - languageName: node - linkType: hard - -"libsodium-wrappers@npm:^0.7.9": - version: 0.7.9 - resolution: "libsodium-wrappers@npm:0.7.9" - dependencies: - libsodium: ^0.7.0 - checksum: b5b1b9e1b4aa5662e07df244934125f9e3cd2ba7fe0ec45191a5ffc822d22f4d2f6e09e42d91c30c4f48ca0c7f810a176fdf5e32eed6722d7d82a2a719459f56 - languageName: node - linkType: hard - -"libsodium@npm:^0.7.0, libsodium@npm:^0.7.9": - version: 0.7.9 - resolution: "libsodium@npm:0.7.9" - checksum: 1c922c9972cf97ddb7207ee4f810dd291e0610dd57ea0e47f2343968392546aaa629945a2fb39ae5f19d067f6ed0bb7330f32cc9a680a847a662e9a210ce7bfb - languageName: node - linkType: hard - -"lodash@npm:^4.17.15": - version: 4.17.21 - resolution: "lodash@npm:4.17.21" - checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 - languageName: node - linkType: hard - -"long@npm:^4.0.0": - version: 4.0.0 - resolution: "long@npm:4.0.0" - checksum: 16afbe8f749c7c849db1f4de4e2e6a31ac6e617cead3bdc4f9605cb703cd20e1e9fc1a7baba674ffcca57d660a6e5b53a9e236d7b25a295d3855cca79cc06744 - languageName: node - linkType: hard - -"lru-cache@npm:^6.0.0": - version: 6.0.0 - resolution: "lru-cache@npm:6.0.0" - dependencies: - yallist: ^4.0.0 - checksum: f97f499f898f23e4585742138a22f22526254fdba6d75d41a1c2526b3b6cc5747ef59c5612ba7375f42aca4f8461950e925ba08c991ead0651b4918b7c978297 - languageName: node - linkType: hard - -"lru-cache@npm:^7.7.1": - version: 7.10.1 - resolution: "lru-cache@npm:7.10.1" - checksum: e8b190d71ed0fcd7b29c71a3e9b01f851c92d1ef8865ff06b5581ca991db1e5e006920ed4da8b56da1910664ed51abfd76c46fb55e82ac252ff6c970ff910d72 - languageName: node - linkType: hard - -"make-fetch-happen@npm:^10.0.3": - version: 10.1.3 - resolution: "make-fetch-happen@npm:10.1.3" - dependencies: - agentkeepalive: ^4.2.1 - cacache: ^16.0.2 - http-cache-semantics: ^4.1.0 - http-proxy-agent: ^5.0.0 - https-proxy-agent: ^5.0.0 - is-lambda: ^1.0.1 - lru-cache: ^7.7.1 - minipass: ^3.1.6 - minipass-collect: ^1.0.2 - minipass-fetch: ^2.0.3 - minipass-flush: ^1.0.5 - minipass-pipeline: ^1.2.4 - negotiator: ^0.6.3 - promise-retry: ^2.0.1 - socks-proxy-agent: ^6.1.1 - ssri: ^9.0.0 - checksum: 14b9bc5fb65a1a1f53b4579c947d1ebdb18db71eb0b35a2eab612e9642a14127917528fe4ffb2c37aaa0d27dfd7507e4044e6e2e47b43985e8fa18722f535b8f - languageName: node - linkType: hard - -"minimatch@npm:^3.1.1": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" - dependencies: - brace-expansion: ^1.1.7 - checksum: c154e566406683e7bcb746e000b84d74465b3a832c45d59912b9b55cd50dee66e5c4b1e5566dba26154040e51672f9aa450a9aef0c97cfc7336b78b7afb9540a - languageName: node - linkType: hard - -"minimatch@npm:^5.0.1": - version: 5.1.0 - resolution: "minimatch@npm:5.1.0" - dependencies: - brace-expansion: ^2.0.1 - checksum: 15ce53d31a06361e8b7a629501b5c75491bc2b59712d53e802b1987121d91b433d73fcc5be92974fde66b2b51d8fb28d75a9ae900d249feb792bb1ba2a4f0a90 - languageName: node - linkType: hard - -"minipass-collect@npm:^1.0.2": - version: 1.0.2 - resolution: "minipass-collect@npm:1.0.2" - dependencies: - minipass: ^3.0.0 - checksum: 14df761028f3e47293aee72888f2657695ec66bd7d09cae7ad558da30415fdc4752bbfee66287dcc6fd5e6a2fa3466d6c484dc1cbd986525d9393b9523d97f10 - languageName: node - linkType: hard - -"minipass-fetch@npm:^2.0.3": - version: 2.1.0 - resolution: "minipass-fetch@npm:2.1.0" - dependencies: - encoding: ^0.1.13 - minipass: ^3.1.6 - minipass-sized: ^1.0.3 - minizlib: ^2.1.2 - dependenciesMeta: - encoding: - optional: true - checksum: 1334732859a3f7959ed22589bafd9c40384b885aebb5932328071c33f86b3eb181d54c86919675d1825ab5f1c8e4f328878c863873258d113c29d79a4b0c9c9f - languageName: node - linkType: hard - -"minipass-flush@npm:^1.0.5": - version: 1.0.5 - resolution: "minipass-flush@npm:1.0.5" - dependencies: - minipass: ^3.0.0 - checksum: 56269a0b22bad756a08a94b1ffc36b7c9c5de0735a4dd1ab2b06c066d795cfd1f0ac44a0fcae13eece5589b908ecddc867f04c745c7009be0b566421ea0944cf - languageName: node - linkType: hard - -"minipass-pipeline@npm:^1.2.4": - version: 1.2.4 - resolution: "minipass-pipeline@npm:1.2.4" - dependencies: - minipass: ^3.0.0 - checksum: b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b - languageName: node - linkType: hard - -"minipass-sized@npm:^1.0.3": - version: 1.0.3 - resolution: "minipass-sized@npm:1.0.3" - dependencies: - minipass: ^3.0.0 - checksum: 79076749fcacf21b5d16dd596d32c3b6bf4d6e62abb43868fac21674078505c8b15eaca4e47ed844985a4514854f917d78f588fcd029693709417d8f98b2bd60 - languageName: node - linkType: hard - -"minipass@npm:^3.0.0, minipass@npm:^3.1.1, minipass@npm:^3.1.6": - version: 3.1.6 - resolution: "minipass@npm:3.1.6" - dependencies: - yallist: ^4.0.0 - checksum: 57a04041413a3531a65062452cb5175f93383ef245d6f4a2961d34386eb9aa8ac11ac7f16f791f5e8bbaf1dfb1ef01596870c88e8822215db57aa591a5bb0a77 - languageName: node - linkType: hard - -"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": - version: 2.1.2 - resolution: "minizlib@npm:2.1.2" - dependencies: - minipass: ^3.0.0 - yallist: ^4.0.0 - checksum: f1fdeac0b07cf8f30fcf12f4b586795b97be856edea22b5e9072707be51fc95d41487faec3f265b42973a304fe3a64acd91a44a3826a963e37b37bafde0212c3 - languageName: node - linkType: hard - -"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": - version: 1.0.4 - resolution: "mkdirp@npm:1.0.4" - bin: - mkdirp: bin/cmd.js - checksum: a96865108c6c3b1b8e1d5e9f11843de1e077e57737602de1b82030815f311be11f96f09cce59bd5b903d0b29834733e5313f9301e3ed6d6f6fba2eae0df4298f - languageName: node - linkType: hard - -"ms@npm:2.1.2": - version: 2.1.2 - resolution: "ms@npm:2.1.2" - checksum: 673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f - languageName: node - linkType: hard - -"ms@npm:^2.0.0": - version: 2.1.3 - resolution: "ms@npm:2.1.3" - checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d - languageName: node - linkType: hard - -"nanoid@npm:^3.1.30": - version: 3.2.0 - resolution: "nanoid@npm:3.2.0" - bin: - nanoid: bin/nanoid.cjs - checksum: 3d1d5a69fea84e538057cf64106e713931c4ef32af344068ecff153ff91252f39b0f2b472e09b0dfff43ac3cf520c92938d90e6455121fe93976e23660f4fccc - languageName: node - linkType: hard - -"negotiator@npm:^0.6.3": - version: 0.6.3 - resolution: "negotiator@npm:0.6.3" - checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 - languageName: node - linkType: hard - -"node-gyp@npm:latest": - version: 9.0.0 - resolution: "node-gyp@npm:9.0.0" - dependencies: - env-paths: ^2.2.0 - glob: ^7.1.4 - graceful-fs: ^4.2.6 - make-fetch-happen: ^10.0.3 - nopt: ^5.0.0 - npmlog: ^6.0.0 - rimraf: ^3.0.2 - semver: ^7.3.5 - tar: ^6.1.2 - which: ^2.0.2 - bin: - node-gyp: bin/node-gyp.js - checksum: 4d8ef8860f7e4f4d86c91db3f519d26ed5cc23b48fe54543e2afd86162b4acbd14f21de42a5db344525efb69a991e021b96a68c70c6e2d5f4a5cb770793da6d3 - languageName: node - linkType: hard - -"nopt@npm:^5.0.0": - version: 5.0.0 - resolution: "nopt@npm:5.0.0" - dependencies: - abbrev: 1 - bin: - nopt: bin/nopt.js - checksum: d35fdec187269503843924e0114c0c6533fb54bbf1620d0f28b4b60ba01712d6687f62565c55cc20a504eff0fbe5c63e22340c3fad549ad40469ffb611b04f2f - languageName: node - linkType: hard - -"npmlog@npm:^6.0.0": - version: 6.0.2 - resolution: "npmlog@npm:6.0.2" - dependencies: - are-we-there-yet: ^3.0.0 - console-control-strings: ^1.1.0 - gauge: ^4.0.3 - set-blocking: ^2.0.0 - checksum: ae238cd264a1c3f22091cdd9e2b106f684297d3c184f1146984ecbe18aaa86343953f26b9520dedd1b1372bc0316905b736c1932d778dbeb1fcf5a1001390e2a - languageName: node - linkType: hard - -"object-hash@npm:^1.3.1": - version: 1.3.1 - resolution: "object-hash@npm:1.3.1" - checksum: fdcb957a2f15a9060e30655a9f683ba1fc25dfb8809a73d32e9634bec385a2f1d686c707ac1e5f69fb773bc12df03fb64c77ce3faeed83e35f4eb1946cb1989e - languageName: node - linkType: hard - -"once@npm:^1.3.0": - version: 1.4.0 - resolution: "once@npm:1.4.0" - dependencies: - wrappy: 1 - checksum: cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 - languageName: node - linkType: hard - -"p-map@npm:^4.0.0": - version: 4.0.0 - resolution: "p-map@npm:4.0.0" - dependencies: - aggregate-error: ^3.0.0 - checksum: cb0ab21ec0f32ddffd31dfc250e3afa61e103ef43d957cc45497afe37513634589316de4eb88abdfd969fe6410c22c0b93ab24328833b8eb1ccc087fc0442a1c - languageName: node - linkType: hard - -"path-is-absolute@npm:^1.0.0": - version: 1.0.1 - resolution: "path-is-absolute@npm:1.0.1" - checksum: 060840f92cf8effa293bcc1bea81281bd7d363731d214cbe5c227df207c34cd727430f70c6037b5159c8a870b9157cba65e775446b0ab06fd5ecc7e54615a3b8 - languageName: node - linkType: hard - -"path-parse@npm:^1.0.7": - version: 1.0.7 - resolution: "path-parse@npm:1.0.7" - checksum: 49abf3d81115642938a8700ec580da6e830dde670be21893c62f4e10bd7dd4c3742ddc603fe24f898cba7eb0c6bc1777f8d9ac14185d34540c6d4d80cd9cae8a - languageName: node - linkType: hard - -"pcm-player@npm:^0.0.11": - version: 0.0.11 - resolution: "pcm-player@npm:0.0.11" - checksum: 8b02471e5788f23dbc965e577656a36c77d8a92f9481ddacac2d46c52755653e61bdb7a41698cee1a29e9df0cf8d853495b3968551b025c601ac4a7bf8139d81 - languageName: node - linkType: hard - -"picocolors@npm:^1.0.0": - version: 1.0.0 - resolution: "picocolors@npm:1.0.0" - checksum: a2e8092dd86c8396bdba9f2b5481032848525b3dc295ce9b57896f931e63fc16f79805144321f72976383fc249584672a75cc18d6777c6b757603f372f745981 - languageName: node - linkType: hard - -"postcss@npm:^8.4.5": - version: 8.4.5 - resolution: "postcss@npm:8.4.5" - dependencies: - nanoid: ^3.1.30 - picocolors: ^1.0.0 - source-map-js: ^1.0.1 - checksum: b78abdd89c10f7b48f4bdcd376104a19d6e9c7495ab521729bdb3df315af6c211360e9f06887ad3bc0ab0f61a04b91d68ea11462997c79cced58b9ccd66fac07 - languageName: node - linkType: hard - -"prettier@npm:^2.5.1": - version: 2.5.1 - resolution: "prettier@npm:2.5.1" - bin: - prettier: bin-prettier.js - checksum: 21b9408476ea1c544b0e45d51ceb94a84789ff92095abb710942d780c862d0daebdb29972d47f6b4d0f7ebbfb0ffbf56cc2cfa3e3e9d1cca54864af185b15b66 - languageName: node - linkType: hard - -"promise-inflight@npm:^1.0.1": - version: 1.0.1 - resolution: "promise-inflight@npm:1.0.1" - checksum: 22749483091d2c594261517f4f80e05226d4d5ecc1fc917e1886929da56e22b5718b7f2a75f3807e7a7d471bc3be2907fe92e6e8f373ddf5c64bae35b5af3981 - languageName: node - linkType: hard - -"promise-retry@npm:^2.0.1": - version: 2.0.1 - resolution: "promise-retry@npm:2.0.1" - dependencies: - err-code: ^2.0.2 - retry: ^0.12.0 - checksum: f96a3f6d90b92b568a26f71e966cbbc0f63ab85ea6ff6c81284dc869b41510e6cdef99b6b65f9030f0db422bf7c96652a3fff9f2e8fb4a0f069d8f4430359429 - languageName: node - linkType: hard - -"protobufjs@npm:^6.8.8": - version: 6.11.2 - resolution: "protobufjs@npm:6.11.2" - dependencies: - "@protobufjs/aspromise": ^1.1.2 - "@protobufjs/base64": ^1.1.2 - "@protobufjs/codegen": ^2.0.4 - "@protobufjs/eventemitter": ^1.1.0 - "@protobufjs/fetch": ^1.1.0 - "@protobufjs/float": ^1.0.2 - "@protobufjs/inquire": ^1.1.0 - "@protobufjs/path": ^1.1.2 - "@protobufjs/pool": ^1.1.0 - "@protobufjs/utf8": ^1.1.0 - "@types/long": ^4.0.1 - "@types/node": ">=13.7.0" - long: ^4.0.0 - bin: - pbjs: bin/pbjs - pbts: bin/pbts - checksum: 80e9d9610c3eb66f9eae4e44a1ae30381cedb721b7d5f635d781fe4c507e2c77bb7c879addcd1dda79733d3ae589d9e66fd18d42baf99b35df7382a0f9920795 - languageName: node - linkType: hard - -"readable-stream@npm:^3.6.0": - version: 3.6.0 - resolution: "readable-stream@npm:3.6.0" - dependencies: - inherits: ^2.0.3 - string_decoder: ^1.1.1 - util-deprecate: ^1.0.1 - checksum: d4ea81502d3799439bb955a3a5d1d808592cf3133350ed352aeaa499647858b27b1c4013984900238b0873ec8d0d8defce72469fb7a83e61d53f5ad61cb80dc8 - languageName: node - linkType: hard - -"resolve@npm:^1.20.0": - version: 1.21.0 - resolution: "resolve@npm:1.21.0" - dependencies: - is-core-module: ^2.8.0 - path-parse: ^1.0.7 - supports-preserve-symlinks-flag: ^1.0.0 - bin: - resolve: bin/resolve - checksum: d7d9092a5c04a048bea16c7e5a2eb605ac3e8363a0cc5644de1fde17d5028e8d5f4343aab1d99bd327b98e91a66ea83e242718150c64dfedcb96e5e7aad6c4f5 - languageName: node - linkType: hard - -"resolve@patch:resolve@^1.20.0#~builtin": - version: 1.21.0 - resolution: "resolve@patch:resolve@npm%3A1.21.0#~builtin::version=1.21.0&hash=07638b" - dependencies: - is-core-module: ^2.8.0 - path-parse: ^1.0.7 - supports-preserve-symlinks-flag: ^1.0.0 - bin: - resolve: bin/resolve - checksum: a0a4d1f7409e73190f31f901f8a619960bb3bd4ae38ba3a54c7ea7e1c87758d28a73256bb8d6a35996a903d1bf14f53883f0dcac6c571c063cb8162d813ad26e - languageName: node - linkType: hard - -"retry@npm:^0.12.0": - version: 0.12.0 - resolution: "retry@npm:0.12.0" - checksum: 623bd7d2e5119467ba66202d733ec3c2e2e26568074923bc0585b6b99db14f357e79bdedb63cab56cec47491c4a0da7e6021a7465ca6dc4f481d3898fdd3158c - languageName: node - linkType: hard - -"rimraf@npm:^3.0.2": - version: 3.0.2 - resolution: "rimraf@npm:3.0.2" - dependencies: - glob: ^7.1.3 - bin: - rimraf: bin.js - checksum: 87f4164e396f0171b0a3386cc1877a817f572148ee13a7e113b238e48e8a9f2f31d009a92ec38a591ff1567d9662c6b67fd8818a2dbbaed74bc26a87a2a4a9a0 - languageName: node - linkType: hard - -"rollup@npm:^2.59.0": - version: 2.64.0 - resolution: "rollup@npm:2.64.0" - dependencies: - fsevents: ~2.3.2 - dependenciesMeta: - fsevents: - optional: true - bin: - rollup: dist/bin/rollup - checksum: dc5b28538002ed635ea54af4c2ced05c52146322c61dbe0e84f294ee62e4f232a15760fdcef9bbeb742883edf9bf093ace5389bbdd816d18b9f5740555135180 - languageName: node - linkType: hard - -"safe-buffer@npm:~5.2.0": - version: 5.2.1 - resolution: "safe-buffer@npm:5.2.1" - checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 - languageName: node - linkType: hard - -"safer-buffer@npm:>= 2.1.2 < 3.0.0": - version: 2.1.2 - resolution: "safer-buffer@npm:2.1.2" - checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 - languageName: node - linkType: hard - -"semver@npm:^7.3.5": - version: 7.3.7 - resolution: "semver@npm:7.3.7" - dependencies: - lru-cache: ^6.0.0 - bin: - semver: bin/semver.js - checksum: 2fa3e877568cd6ce769c75c211beaed1f9fce80b28338cadd9d0b6c40f2e2862bafd62c19a6cff42f3d54292b7c623277bcab8816a2b5521cf15210d43e75232 - languageName: node - linkType: hard - -"set-blocking@npm:^2.0.0": - version: 2.0.0 - resolution: "set-blocking@npm:2.0.0" - checksum: 6e65a05f7cf7ebdf8b7c75b101e18c0b7e3dff4940d480efed8aad3a36a4005140b660fa1d804cb8bce911cac290441dc728084a30504d3516ac2ff7ad607b02 - languageName: node - linkType: hard - -"signal-exit@npm:^3.0.7": - version: 3.0.7 - resolution: "signal-exit@npm:3.0.7" - checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 - languageName: node - linkType: hard - -"smart-buffer@npm:^4.2.0": - version: 4.2.0 - resolution: "smart-buffer@npm:4.2.0" - checksum: b5167a7142c1da704c0e3af85c402002b597081dd9575031a90b4f229ca5678e9a36e8a374f1814c8156a725d17008ae3bde63b92f9cfd132526379e580bec8b - languageName: node - linkType: hard - -"socks-proxy-agent@npm:^6.1.1": - version: 6.2.0 - resolution: "socks-proxy-agent@npm:6.2.0" - dependencies: - agent-base: ^6.0.2 - debug: ^4.3.3 - socks: ^2.6.2 - checksum: 6723fd64fb50334e2b340fd0a80fd8488ffc5bc43d85b7cf1d25612044f814dd7d6ea417fd47602159941236f7f4bd15669fa5d7e1f852598a31288e1a43967b - languageName: node - linkType: hard - -"socks@npm:^2.6.2": - version: 2.6.2 - resolution: "socks@npm:2.6.2" - dependencies: - ip: ^1.1.5 - smart-buffer: ^4.2.0 - checksum: dd9194293059d737759d5c69273850ad4149f448426249325c4bea0e340d1cf3d266c3b022694b0dcf5d31f759de23657244c481fc1e8322add80b7985c36b5e - languageName: node - linkType: hard - -"source-map-js@npm:^1.0.1": - version: 1.0.1 - resolution: "source-map-js@npm:1.0.1" - checksum: 22606113d62bbd468712b0cb0c46e9a8629de7eb081049c62a04d977a211abafd7d61455617f8b78daba0b6c0c7e7c88f8c6b5aaeacffac0a6676ecf5caac5ce - languageName: node - linkType: hard - -"ssri@npm:^9.0.0": - version: 9.0.0 - resolution: "ssri@npm:9.0.0" - dependencies: - minipass: ^3.1.1 - checksum: bf33174232d07cc64e77ab1c51b55d28352273380c503d35642a19627e88a2c5f160039bb0a28608a353485075dda084dbf0390c7070f9f284559eb71d01b84b - languageName: node - linkType: hard - -"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.2.3": - version: 4.2.3 - resolution: "string-width@npm:4.2.3" - dependencies: - emoji-regex: ^8.0.0 - is-fullwidth-code-point: ^3.0.0 - strip-ansi: ^6.0.1 - checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb - languageName: node - linkType: hard - -"string_decoder@npm:^1.1.1": - version: 1.3.0 - resolution: "string_decoder@npm:1.3.0" - dependencies: - safe-buffer: ~5.2.0 - checksum: 8417646695a66e73aefc4420eb3b84cc9ffd89572861fe004e6aeb13c7bc00e2f616247505d2dbbef24247c372f70268f594af7126f43548565c68c117bdeb56 - languageName: node - linkType: hard - -"strip-ansi@npm:^6.0.1": - version: 6.0.1 - resolution: "strip-ansi@npm:6.0.1" - dependencies: - ansi-regex: ^5.0.1 - checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c - languageName: node - linkType: hard - -"supports-preserve-symlinks-flag@npm:^1.0.0": - version: 1.0.0 - resolution: "supports-preserve-symlinks-flag@npm:1.0.0" - checksum: 53b1e247e68e05db7b3808b99b892bd36fb096e6fba213a06da7fab22045e97597db425c724f2bbd6c99a3c295e1e73f3e4de78592289f38431049e1277ca0ae - languageName: node - linkType: hard - -"tar@npm:^6.1.11, tar@npm:^6.1.2": - version: 6.1.11 - resolution: "tar@npm:6.1.11" - dependencies: - chownr: ^2.0.0 - fs-minipass: ^2.0.0 - minipass: ^3.0.0 - minizlib: ^2.1.1 - mkdirp: ^1.0.3 - yallist: ^4.0.0 - checksum: a04c07bb9e2d8f46776517d4618f2406fb977a74d914ad98b264fc3db0fe8224da5bec11e5f8902c5b9bcb8ace22d95fbe3c7b36b8593b7dfc8391a25898f32f - languageName: node - linkType: hard - -"ts-poet@npm:^4.5.0": - version: 4.10.0 - resolution: "ts-poet@npm:4.10.0" - dependencies: - lodash: ^4.17.15 - prettier: ^2.5.1 - checksum: ffb3890a429f7ab59d96a7a17d9cc161bce786695af0fd77156e5779cfaeda92eaae4f15995a8c71a83cfb528d61bd2518bae19c4adec147bff8dce6f27c57d3 - languageName: node - linkType: hard - -"ts-proto-descriptors@npm:^1.2.1": - version: 1.3.1 - resolution: "ts-proto-descriptors@npm:1.3.1" - dependencies: - long: ^4.0.0 - protobufjs: ^6.8.8 - checksum: ef8acf9231375dd00cfa667c688746ae24fb8012a3875d1447cb6a6e9e0311150681719072716f58a24b1df801bcc35e56faca11ea4bac1f8146038b524b93c4 - languageName: node - linkType: hard - -"ts-proto@npm:^1.101.0": - version: 1.101.0 - resolution: "ts-proto@npm:1.101.0" - dependencies: - "@types/object-hash": ^1.3.0 - dataloader: ^1.4.0 - object-hash: ^1.3.1 - protobufjs: ^6.8.8 - ts-poet: ^4.5.0 - ts-proto-descriptors: ^1.2.1 - bin: - protoc-gen-ts_proto: protoc-gen-ts_proto - checksum: d404e34cad4fc5fb19271f7f257ff177d0ebac22ceca3b287927566a3ecda2f350b8592851d27415f6ec645525eae4ab40291ce3a6a3e151bb004478a1fe634a - languageName: node - linkType: hard - -"typescript@npm:^4.4.4": - version: 4.5.4 - resolution: "typescript@npm:4.5.4" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 59f3243f9cd6fe3161e6150ff6bf795fc843b4234a655dbd938a310515e0d61afd1ac942799e7415e4334255e41c2c49b7dd5d9fd38a17acd25a6779ca7e0961 - languageName: node - linkType: hard - -"typescript@patch:typescript@^4.4.4#~builtin": - version: 4.5.4 - resolution: "typescript@patch:typescript@npm%3A4.5.4#~builtin::version=4.5.4&hash=bda367" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: eda87927f9cfb94aca9b5e47842daf37347ad3073133e17f556fbb6c18f3493c5b551eedab0f4b26774235ddb7acbe0087250d5285f72ce6819a0891dd5a74ed - languageName: node - linkType: hard - -"unique-filename@npm:^1.1.1": - version: 1.1.1 - resolution: "unique-filename@npm:1.1.1" - dependencies: - unique-slug: ^2.0.0 - checksum: cf4998c9228cc7647ba7814e255dec51be43673903897b1786eff2ac2d670f54d4d733357eb08dea969aa5e6875d0e1bd391d668fbdb5a179744e7c7551a6f80 - languageName: node - linkType: hard - -"unique-slug@npm:^2.0.0": - version: 2.0.2 - resolution: "unique-slug@npm:2.0.2" - dependencies: - imurmurhash: ^0.1.4 - checksum: 5b6876a645da08d505dedb970d1571f6cebdf87044cb6b740c8dbb24f0d6e1dc8bdbf46825fd09f994d7cf50760e6f6e063cfa197d51c5902c00a861702eb75a - languageName: node - linkType: hard - -"util-deprecate@npm:^1.0.1": - version: 1.0.2 - resolution: "util-deprecate@npm:1.0.2" - checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 - languageName: node - linkType: hard - -"vite@npm:^2.7.2": - version: 2.7.12 - resolution: "vite@npm:2.7.12" - dependencies: - esbuild: ^0.13.12 - fsevents: ~2.3.2 - postcss: ^8.4.5 - resolve: ^1.20.0 - rollup: ^2.59.0 - peerDependencies: - less: "*" - sass: "*" - stylus: "*" - dependenciesMeta: - fsevents: - optional: true - peerDependenciesMeta: - less: - optional: true - sass: - optional: true - stylus: - optional: true - bin: - vite: bin/vite.js - checksum: 56d62ae8131b02891f2dbd81f26a3ca28a02bfe390f9cb4e0c2d8dc831c2e2f8264dd3c45b14c7dd48e79d83d323a35148f92729e1f3385fae04fcd691f3f985 - languageName: node - linkType: hard - -"wasm-feature-detect@npm:^1.2.11": - version: 1.2.11 - resolution: "wasm-feature-detect@npm:1.2.11" - checksum: e7f28f5e6ca0722ba059e200c47a944ebd7570027a3ac5600b7178ee9bf950fe5280a68b5e3b5f29930407cc1214695ca10ea36a3d995d3445f4e34db58a8505 - languageName: node - linkType: hard - -"web_hbb@workspace:.": - version: 0.0.0-use.local - resolution: "web_hbb@workspace:." - dependencies: - fast-sha256: ^1.3.0 - libsodium: ^0.7.9 - libsodium-wrappers: ^0.7.9 - pcm-player: ^0.0.11 - ts-proto: ^1.101.0 - typescript: ^4.4.4 - vite: ^2.7.2 - wasm-feature-detect: ^1.2.11 - zstddec: ^0.0.2 - languageName: unknown - linkType: soft - -"which@npm:^2.0.2": - version: 2.0.2 - resolution: "which@npm:2.0.2" - dependencies: - isexe: ^2.0.0 - bin: - node-which: ./bin/node-which - checksum: 1a5c563d3c1b52d5f893c8b61afe11abc3bab4afac492e8da5bde69d550de701cf9806235f20a47b5c8fa8a1d6a9135841de2596535e998027a54589000e66d1 - languageName: node - linkType: hard - -"wide-align@npm:^1.1.5": - version: 1.1.5 - resolution: "wide-align@npm:1.1.5" - dependencies: - string-width: ^1.0.2 || 2 || 3 || 4 - checksum: d5fc37cd561f9daee3c80e03b92ed3e84d80dde3365a8767263d03dacfc8fa06b065ffe1df00d8c2a09f731482fcacae745abfbb478d4af36d0a891fad4834d3 - languageName: node - linkType: hard - -"wrappy@npm:1": - version: 1.0.2 - resolution: "wrappy@npm:1.0.2" - checksum: 159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 - languageName: node - linkType: hard - -"yallist@npm:^4.0.0": - version: 4.0.0 - resolution: "yallist@npm:4.0.0" - checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 - languageName: node - linkType: hard - -"zstddec@npm:^0.0.2": - version: 0.0.2 - resolution: "zstddec@npm:0.0.2" - checksum: 107334442a34590173cda03614006337712658fd043fa79f72bd486de527e2a16da474d7b20d4a171f086b334c2ad8a72afb634776d79bc2c36aee065babe31b - languageName: node - linkType: hard diff --git a/flutter/web/v1/libs/firebase-analytics.js b/flutter/web/v1/libs/firebase-analytics.js deleted file mode 100644 index 9b9a02b093f..00000000000 --- a/flutter/web/v1/libs/firebase-analytics.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("@firebase/app")):"function"==typeof define&&define.amd?define(["@firebase/app"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).firebase)}(this,function(mt){"use strict";try{!function(){function e(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=e(mt),r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)};var s=function(){return(s=Object.assign||function(e){for(var t,n=1,r=arguments.length;na[0]&&t[1]=e.length?void 0:e)&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function u(e,t){for(var n=0,r=t.length,i=e.length;n"})):"Error",e=this.serviceName+": "+e+" ("+o+").";return new f(o,e,i)},n);function n(e,t,n){this.service=e,this.serviceName=t,this.errors=n}var y=/\{\$([^}]+)}/g,b=1e3,w=2,I=144e5,_=.5;function E(e,t,n){void 0===n&&(n=w);n=(t=void 0===t?b:t)*Math.pow(n,e),e=Math.round(_*n*(Math.random()-.5)*2);return Math.min(I,n+e)}var T=(S.prototype.setInstantiationMode=function(e){return this.instantiationMode=e,this},S.prototype.setMultipleInstances=function(e){return this.multipleInstances=e,this},S.prototype.setServiceProps=function(e){return this.serviceProps=e,this},S.prototype.setInstanceCreatedCallback=function(e){return this.onInstanceCreated=e,this},S);function S(e,t,n){this.name=e,this.instanceFactory=t,this.type=n,this.multipleInstances=!1,this.serviceProps={},this.instantiationMode="LAZY",this.onInstanceCreated=null}function C(n){return new Promise(function(e,t){n.onsuccess=function(){e(n.result)},n.onerror=function(){t(n.error)}})}function O(n,r,i){var o,e=new Promise(function(e,t){C(o=n[r].apply(n,i)).then(e,t)});return e.request=o,e}function N(e,n,t){t.forEach(function(t){Object.defineProperty(e.prototype,t,{get:function(){return this[n][t]},set:function(e){this[n][t]=e}})})}function D(t,n,r,e){e.forEach(function(e){e in r.prototype&&(t.prototype[e]=function(){return O(this[n],e,arguments)})})}function P(t,n,r,e){e.forEach(function(e){e in r.prototype&&(t.prototype[e]=function(){return this[n][e].apply(this[n],arguments)})})}function A(e,r,t,n){n.forEach(function(n){n in t.prototype&&(e.prototype[n]=function(){return e=this[r],(t=O(e,n,arguments)).then(function(e){if(e)return new k(e,t.request)});var e,t})})}function x(e){this._index=e}function k(e,t){this._cursor=e,this._request=t}function j(e){this._store=e}function L(n){this._tx=n,this.complete=new Promise(function(e,t){n.oncomplete=function(){e()},n.onerror=function(){t(n.error)},n.onabort=function(){t(n.error)}})}function R(e,t,n){this._db=e,this.oldVersion=t,this.transaction=new L(n)}function F(e){this._db=e}N(x,"_index",["name","keyPath","multiEntry","unique"]),D(x,"_index",IDBIndex,["get","getKey","getAll","getAllKeys","count"]),A(x,"_index",IDBIndex,["openCursor","openKeyCursor"]),N(k,"_cursor",["direction","key","primaryKey","value"]),D(k,"_cursor",IDBCursor,["update","delete"]),["advance","continue","continuePrimaryKey"].forEach(function(n){n in IDBCursor.prototype&&(k.prototype[n]=function(){var t=this,e=arguments;return Promise.resolve().then(function(){return t._cursor[n].apply(t._cursor,e),C(t._request).then(function(e){if(e)return new k(e,t._request)})})})}),j.prototype.createIndex=function(){return new x(this._store.createIndex.apply(this._store,arguments))},j.prototype.index=function(){return new x(this._store.index.apply(this._store,arguments))},N(j,"_store",["name","keyPath","indexNames","autoIncrement"]),D(j,"_store",IDBObjectStore,["put","add","delete","clear","get","getAll","getKey","getAllKeys","count"]),A(j,"_store",IDBObjectStore,["openCursor","openKeyCursor"]),P(j,"_store",IDBObjectStore,["deleteIndex"]),L.prototype.objectStore=function(){return new j(this._tx.objectStore.apply(this._tx,arguments))},N(L,"_tx",["objectStoreNames","mode"]),P(L,"_tx",IDBTransaction,["abort"]),R.prototype.createObjectStore=function(){return new j(this._db.createObjectStore.apply(this._db,arguments))},N(R,"_db",["name","version","objectStoreNames"]),P(R,"_db",IDBDatabase,["deleteObjectStore","close"]),F.prototype.transaction=function(){return new L(this._db.transaction.apply(this._db,arguments))},N(F,"_db",["name","version","objectStoreNames"]),P(F,"_db",IDBDatabase,["close"]),["openCursor","openKeyCursor"].forEach(function(i){[j,x].forEach(function(e){i in e.prototype&&(e.prototype[i.replace("open","iterate")]=function(){var e=(n=arguments,Array.prototype.slice.call(n)),t=e[e.length-1],n=this._store||this._index,r=n[i].apply(n,e.slice(0,-1));r.onsuccess=function(){t(r.result)}})})}),[x,j].forEach(function(e){e.prototype.getAll||(e.prototype.getAll=function(e,n){var r=this,i=[];return new Promise(function(t){r.iterateCursor(e,function(e){e?(i.push(e.value),void 0===n||i.length!=n?e.continue():t(i)):t(i)})})})});var M="0.4.32",B=1e4,H="w:"+M,q="FIS_v2",V="https://firebaseinstallations.googleapis.com/v1",G=36e5,K=((Re={})["missing-app-config-values"]='Missing App configuration value: "{$valueName}"',Re["not-registered"]="Firebase Installation is not registered.",Re["installation-not-found"]="Firebase Installation not found.",Re["request-failed"]='{$requestName} request failed with error "{$serverCode} {$serverStatus}: {$serverMessage}"',Re["app-offline"]="Could not process request. Application offline.",Re["delete-pending-registration"]="Can't delete installation while there is a pending registration request.",Re),U=new m("installations","Installations",K);function W(e){return e instanceof f&&e.code.includes("request-failed")}function $(e){e=e.projectId;return V+"/projects/"+e+"/installations"}function z(e){return{token:e.token,requestStatus:2,expiresIn:(e=e.expiresIn,Number(e.replace("s","000"))),creationTime:Date.now()}}function J(n,r){return p(this,void 0,void 0,function(){var t;return h(this,function(e){switch(e.label){case 0:return[4,r.json()];case 1:return t=e.sent(),t=t.error,[2,U.create("request-failed",{requestName:n,serverCode:t.code,serverMessage:t.message,serverStatus:t.status})]}})})}function Y(e){e=e.apiKey;return new Headers({"Content-Type":"application/json",Accept:"application/json","x-goog-api-key":e})}function X(e,t){t=t.refreshToken,e=Y(e);return e.append("Authorization",q+" "+t),e}function Z(n){return p(this,void 0,void 0,function(){var t;return h(this,function(e){switch(e.label){case 0:return[4,n()];case 1:return 500<=(t=e.sent()).status&&t.status<600?[2,n()]:[2,t]}})})}function Q(t){return new Promise(function(e){setTimeout(e,t)})}function ee(e){return btoa(String.fromCharCode.apply(String,u([],function(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||0a[0]&&t[1]=e.length?void 0:e)&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function f(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||0"})):"Error",e=this.serviceName+": "+e+" ("+o+").";return new c(o,e,i)},v);function v(e,t,n){this.service=e,this.serviceName=t,this.errors=n}var m=/\{\$([^}]+)}/g;function y(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function g(e,t){t=new b(e,t);return t.subscribe.bind(t)}var b=(I.prototype.next=function(t){this.forEachObserver(function(e){e.next(t)})},I.prototype.error=function(t){this.forEachObserver(function(e){e.error(t)}),this.close(t)},I.prototype.complete=function(){this.forEachObserver(function(e){e.complete()}),this.close()},I.prototype.subscribe=function(e,t,n){var r,i=this;if(void 0===e&&void 0===t&&void 0===n)throw new Error("Missing Observer.");void 0===(r=function(e,t){if("object"!=typeof e||null===e)return!1;for(var n=0,r=t;n=(null!=o?o:e.logLevel)&&a({level:R[t].toLowerCase(),message:i,args:n,type:e.name})}}(n[e])}var H=((H={})["no-app"]="No Firebase App '{$appName}' has been created - call Firebase App.initializeApp()",H["bad-app-name"]="Illegal App name: '{$appName}",H["duplicate-app"]="Firebase App named '{$appName}' already exists",H["app-deleted"]="Firebase App named '{$appName}' already deleted",H["invalid-app-argument"]="firebase.{$appName}() takes either no argument or a Firebase App instance.",H["invalid-log-argument"]="First argument to `onLog` must be null or a function.",H),V=new d("app","Firebase",H),B="@firebase/app",M="[DEFAULT]",U=((H={})[B]="fire-core",H["@firebase/analytics"]="fire-analytics",H["@firebase/app-check"]="fire-app-check",H["@firebase/auth"]="fire-auth",H["@firebase/database"]="fire-rtdb",H["@firebase/functions"]="fire-fn",H["@firebase/installations"]="fire-iid",H["@firebase/messaging"]="fire-fcm",H["@firebase/performance"]="fire-perf",H["@firebase/remote-config"]="fire-rc",H["@firebase/storage"]="fire-gcs",H["@firebase/firestore"]="fire-fst",H["fire-js"]="fire-js",H["firebase-wrapper"]="fire-js-all",H),W=new z("@firebase/app"),G=(Object.defineProperty($.prototype,"automaticDataCollectionEnabled",{get:function(){return this.checkDestroyed_(),this.automaticDataCollectionEnabled_},set:function(e){this.checkDestroyed_(),this.automaticDataCollectionEnabled_=e},enumerable:!1,configurable:!0}),Object.defineProperty($.prototype,"name",{get:function(){return this.checkDestroyed_(),this.name_},enumerable:!1,configurable:!0}),Object.defineProperty($.prototype,"options",{get:function(){return this.checkDestroyed_(),this.options_},enumerable:!1,configurable:!0}),$.prototype.delete=function(){var t=this;return new Promise(function(e){t.checkDestroyed_(),e()}).then(function(){return t.firebase_.INTERNAL.removeApp(t.name_),Promise.all(t.container.getProviders().map(function(e){return e.delete()}))}).then(function(){t.isDeleted_=!0})},$.prototype._getService=function(e,t){void 0===t&&(t=M),this.checkDestroyed_();var n=this.container.getProvider(e);return n.isInitialized()||"EXPLICIT"!==(null===(e=n.getComponent())||void 0===e?void 0:e.instantiationMode)||n.initialize(),n.getImmediate({identifier:t})},$.prototype._removeServiceInstance=function(e,t){void 0===t&&(t=M),this.container.getProvider(e).clearInstance(t)},$.prototype._addComponent=function(t){try{this.container.addComponent(t)}catch(e){W.debug("Component "+t.name+" failed to register with FirebaseApp "+this.name,e)}},$.prototype._addOrOverwriteComponent=function(e){this.container.addOrOverwriteComponent(e)},$.prototype.toJSON=function(){return{name:this.name,automaticDataCollectionEnabled:this.automaticDataCollectionEnabled,options:this.options}},$.prototype.checkDestroyed_=function(){if(this.isDeleted_)throw V.create("app-deleted",{appName:this.name_})},$);function $(e,t,n){var r=this;this.firebase_=n,this.isDeleted_=!1,this.name_=t.name,this.automaticDataCollectionEnabled_=t.automaticDataCollectionEnabled||!1,this.options_=h(void 0,e),this.container=new S(t.name),this._addComponent(new O("app",function(){return r},"PUBLIC")),this.firebase_.INTERNAL.components.forEach(function(e){return r._addComponent(e)})}G.prototype.name&&G.prototype.options||G.prototype.delete||console.log("dc");var K="8.10.1";function Y(a){var s={},l=new Map,c={__esModule:!0,initializeApp:function(e,t){void 0===t&&(t={});"object"==typeof t&&null!==t||(t={name:t});var n=t;void 0===n.name&&(n.name=M);t=n.name;if("string"!=typeof t||!t)throw V.create("bad-app-name",{appName:String(t)});if(y(s,t))throw V.create("duplicate-app",{appName:t});n=new a(e,n,c);return s[t]=n},app:u,registerVersion:function(e,t,n){var r=null!==(i=U[e])&&void 0!==i?i:e;n&&(r+="-"+n);var i=r.match(/\s|\//),e=t.match(/\s|\//);i||e?(n=['Unable to register library "'+r+'" with version "'+t+'":'],i&&n.push('library name "'+r+'" contains illegal characters (whitespace or "/")'),i&&e&&n.push("and"),e&&n.push('version name "'+t+'" contains illegal characters (whitespace or "/")'),W.warn(n.join(" "))):o(new O(r+"-version",function(){return{library:r,version:t}},"VERSION"))},setLogLevel:T,onLog:function(e,t){if(null!==e&&"function"!=typeof e)throw V.create("invalid-log-argument");x(e,t)},apps:null,SDK_VERSION:K,INTERNAL:{registerComponent:o,removeApp:function(e){delete s[e]},components:l,useAsService:function(e,t){return"serverAuth"!==t?t:null}}};function u(e){if(!y(s,e=e||M))throw V.create("no-app",{appName:e});return s[e]}function o(n){var e,r=n.name;if(l.has(r))return W.debug("There were multiple attempts to register component "+r+"."),"PUBLIC"===n.type?c[r]:null;l.set(r,n),"PUBLIC"===n.type&&(e=function(e){if("function"!=typeof(e=void 0===e?u():e)[r])throw V.create("invalid-app-argument",{appName:r});return e[r]()},void 0!==n.serviceProps&&h(e,n.serviceProps),c[r]=e,a.prototype[r]=function(){for(var e=[],t=0;t 30) { - console.log('yuv: ' + parseInt('' + testSpeed[1] / testSpeed[0])); - testSpeed = [0, 0]; - } - return out; -} - -var currentFrame; -self.addEventListener('message', (e) => { - currentFrame = e.data; -}); - -function run() { - if (currentFrame) { - self.postMessage(I420ToARGB(currentFrame)); - currentFrame = undefined; - } - setTimeout(run, 1); -} - -run(); \ No newline at end of file diff --git a/flutter/web/v1/yuv.wasm b/flutter/web/v1/yuv.wasm deleted file mode 100644 index f203c685c932a7c62cccb523540555965bd1a695..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8238 zcmcJUzmFuxb;ql!yXVLD3~zK?ip$%juBIixXMlzXLO1~qtSv&^^Z9U)VP{gXJW)%V zGe@3wZn>fWgR`<}*ntBD5}-f<0|6EU1qvj%I4V-}hdPyZ7*Q@0|0y<;#BQmoGd1_e=Nk)^hd76kk%R4{q}HH%~wF!t;lVzxnbDpE-PZ z@)hTwIXryrjjz16IC|ye@YOfpc;)1^2X7pn-23wVBkv}MhewOW8xIceKX~PyKDes) z`n~(SxqkWT;^@c?h7V8fy)ysnd#6V(jO_WZjvk&oSR8RYdrdaH-1^C%4fkAh>5;d8 z{(QG_N$>72cIi8=-zojm%jlDzHyr(nm?OfjV_k6>6054^eMKz`W!D$L?om)PUi7r_f!AP zW-DHgPx2|9Egv@Z{nUH28P6u}PhDW>R;=T*_v0tx6Yb}jraUp%ci(=GHm9~Zjy{(6 zo6G0bdNw-Gtn4|@DC00*xp1Iv{~`N#d+E8`V;IMpv$0FJhrV=s3^{x5FXNNROTSmu zT#Z*!kW!r*hZV8Q5aZJL_!OPZQ*rzD*wr;g;B6E?Zf|wzKI!TnxcHQ-CpET>8$SzU9L*eQR(dxW=wAR z1=908r)B-W&-r@!Nyf~Z`cq84>@Vwo=ASXHFHPvFXX&kDwYPVhekx9zaN77( z9D_NZmJgFZj)+vG;)L$^1~Z+sl;}EPc_R!O?4JH(Fl+pQEggI5?O1&6m~)MM4KV%9 zFp55gn_*DLGPX+it zOukD@%#y`44IwQmH9>irp{Xo;s~3BTO2HSbEy#X^ccph_I2;8Yn7mH({4qXgKoV zqnidquZ5kRsbuD1)ReabTv^PeA4eerpdQE2m zp178-^Z4k|qdt>G|AW&;i=6%XbF(xtC=2`Q`NehX0m>l3dd{v`kHCDAuAE9Yi|7`( zMDizaze`WePVwPI+MCr)Z^INpIC4|@s2Yyo(lbKY>70MBAMBn?eP^D|5>+g|fPi5N zvYG-eau_7AMuEYy^(rtZLU>jlhs`HVI3SkAYL`8ThpZZdQ6>6Z?9Lxhnr zSpE;>;A=HZv7!O4*x!Vl2X0fvGsMfFEA>!n%%t%pp&fXD*4lsot-XX^ip*QsSy}=n zJJTLc$it}d!llvWCN5iQG+Yi@WeqpELk5~uk&(B?KCUlS^{8OAgWTXy}$|LHp@hGbDn3STmI+f2CIE zI~%>Y{4C4ol4XgwqE<_^%&S54H0@kFqEq;0Ss@i*@5?I15Uc zHx)r8_V12|=|8o0Q?ko@_JqiaUBz8+Qr=mNp@9!-G82G5F6HN~Km{{?UQZnm1Jqi> zqjx4rMR5O}vE0WfOOBD;q=#*m2V*>G+Cu_ODRP|iipv0VE(^^#VKG0RO?!3haYp?1 z*Ty4@adpXQdol~B1)7{zbJ&PwZjJq@uN4OKh_u}zm2ck%G8IFf7pP$9v zU2|g7JIWZw-dxA>b2zF!4JUR}+rm$JX=wx0KL@s6mq78doq11}-@l zjUQFwZ2E&#%ztPkl?s347ws;Du+S>bG)Pi(02PuxE%>ApHRvean2}V}STGxra~R;$ z`&u(P!Sa2Mp`~TQl@Y785wk`bvt>!Hb}$ZW^Nl#7#hu0>m$u&($^9@pnWS;)7bsgm z`#Dmn{plbr)&6XdajfTGGL*JJcS^kqB_sHe@+<{$9^a?K9-(@KNbwfvkx%u~4W$=v zA#&;;a|&guJW->}Z8Esj$yQz#R;dW-=M9pu^lyXo&IpCD69OqiACXc4-|CKXs0y7* zIB0vO;jCA&U)Anct@g`@WMnGacU)hsO9d|b1TP)Cjc z@~s$JO@vkxunECAE4fNV8>XWD1wUOQDbu+>e0%KkZ%hH8w09QuUQ01-vV`ptaZE6m~hv49IU zhgz6pL4`S{DqKoZf(pU6+k6k?=X}%lFuAi4{n(R^<&11yp&(8_P11Jkvn+PmZG)Y+ zER^#3X4sJ^L{?-nIjEYn;*QB}`6}-caC`Lu3ya?{wbIF~mNLCfO;i%{I)4i?mbq|f zEkX=kn_?OWY`Ih;7)M8j)FuCHXvx1aVC7vT|MDl$A4-ELDMi>h6$sg^ZB?yZ+-~Digoi zppI=))!ON|wra(*u`8!e*)K2%%9*qLh}-G8mPtSM#PVzIj3K z=(mMq`TN0<>QivEB9x5;M~GLtE;Agfj1RV>`VlI*Sa=MTziaU$HJnGKZN5i$CI}T{ zEgbPk`PT*w|s-O0Oj1`yQ6 zz?6M=DdWJss?*;k5rBp|RpJpCC%RX}{Qy{f0-Q=)w|ZQM3YvQM8A;iwN4!#yEhcUaWs(t&Bncm?qo`iUMO7!t=t3H z^_Ry10D0j0ugvLc_mOkzE-}M{!OAnyCv6YeyUjQ7sH8^p$fY>LT6$cit577pxgb~( zWU)z*2u5inr$H31-Had+d6E?XBI)05f;4Zs*ls__hq*x3`6(3&%1D`7XO$PWiGmE8 zFN9D+E-|jKVytx4)y^h-S|ZMUyU<4Du^S!L5!5=u&brodKwUem*X=Am)-_tbiD@sH z*r>e;*HXHt(o(A`=<%gCk0|^_@aGJFx$x0-`K3tW!{dcdizM`W5grnpJ3dw>cCT*v z^6D+3uRS(Nqt9VVN#I9-DG6xD_-!$DKIm4Pm=fV;Od-^zn4;e>1z_c09i{}0#{jc( zYKXQ)m`wZsVT=;I^?s{TSHFz7RcRDpIBvr?eizB9n}sIb1JnvPf{bDp%}7VfjR=8X z(XKP1=t`9?n!x50;;2st0QBaE**taEc7~s-jJ&! zMxP)sG-IKc>No#1V-zI?P1qoZF!(VmipC0k)>HJgUJ7*Sl<04qlS7B8c2Mg|>!->v y?bb3aqocs#ZnJLaIvdnikH7fbm(JWbUVY(jzdiY(&LocalConfig::get_option("user_info")) @@ -2522,6 +2542,11 @@ impl LoginConfigHandler { port: self.port_forward.1, ..Default::default() }), + ConnType::TERMINAL => { + let mut terminal = Terminal::new(); + terminal.service_id = self.get_option("terminal-service-id"); + lr.set_terminal(terminal); + } _ => {} } @@ -3237,8 +3262,7 @@ pub async fn handle_hash( } if password.is_empty() { - let p = - crate::ui_interface::get_builtin_option(config::keys::OPTION_DEFAULT_CONNECT_PASSWORD); + let p = crate::ui_interface::get_builtin_option(keys::OPTION_DEFAULT_CONNECT_PASSWORD); if !p.is_empty() { let mut hasher = Sha256::new(); hasher.update(p.clone()); @@ -3789,11 +3813,9 @@ pub mod peer_online { } // Retry for 2 times to get the online response for _ in 0..2 { - if let Some(msg_in) = crate::get_next_nonkeyexchange_msg( - &mut socket, - Some(timeout.as_millis() as _), - ) - .await + if let Some(msg_in) = + crate::get_next_nonkeyexchange_msg(&mut socket, Some(timeout.as_millis() as _)) + .await { match msg_in.union { Some(rendezvous_message::Union::OnlineResponse(online_response)) => { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 39f9185a09b..e2838cc2218 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -85,6 +85,7 @@ struct ParsedPeerInfo { is_installed: bool, idd_impl: String, support_view_camera: bool, + support_terminal: bool, } impl ParsedPeerInfo { @@ -131,10 +132,7 @@ impl Remote { #[cfg(target_os = "windows")] 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() - && !self.handler.is_view_camera() - { + if self.handler.is_default() { // It is ok to call this function multiple times. ContextSend::enable(true); Some(crate::SimpleCallOnReturn { @@ -159,6 +157,8 @@ impl Remote { ConnType::FILE_TRANSFER } else if self.handler.is_view_camera() { ConnType::VIEW_CAMERA + } else if self.handler.is_terminal() { + ConnType::TERMINAL } else { ConnType::default() }; @@ -195,11 +195,7 @@ impl Remote { let mut rx_clip_client_holder = (Arc::new(TokioMutex::new(rx)), None); #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] { - let is_conn_not_default = self.handler.is_file_transfer() - || self.handler.is_port_forward() - || self.handler.is_rdp() - || self.handler.is_view_camera(); - if !is_conn_not_default { + if self.handler.is_default() { (self.client_conn_id, rx_clip_client_holder.0) = clipboard::get_rx_cliprdr_client(&self.handler.get_id()); log::debug!("get cliprdr client for conn_id {}", self.client_conn_id); @@ -338,12 +334,12 @@ impl Remote { .set_disconnected(round); #[cfg(not(target_os = "ios"))] - if !self.handler.is_view_camera() && _set_disconnected_ok { + if self.handler.is_default() && _set_disconnected_ok { Client::try_stop_clipboard(); } #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] - if !self.handler.is_view_camera() && _set_disconnected_ok { + if self.handler.is_default() && _set_disconnected_ok { crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id); } } @@ -437,7 +433,10 @@ impl Remote { // Start a voice call recorder, records audio and send to remote fn start_voice_call(&mut self) -> Option> { - if self.handler.is_file_transfer() || self.handler.is_port_forward() { + if self.handler.is_file_transfer() + || self.handler.is_port_forward() + || self.handler.is_terminal() + { return None; } // iOS does not have this server. @@ -1230,6 +1229,24 @@ impl Remote { return false; } + fn check_terminal_support(&self, peer_version: &str) -> bool { + if self.peer_info.support_terminal { + return true; + } + if hbb_common::get_version_number(&peer_version) < hbb_common::get_version_number("1.4.1") { + self.handler.msgbox( + "error", + "Remote terminal not supported", + "Remote terminal is not supported by the remote side. Please upgrade to version 1.4.1 or higher.", + "", + ); + } else { + self.handler + .on_error("Remote terminal is not supported by the remote side"); + } + return false; + } + async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { if let Ok(msg_in) = Message::parse_from_bytes(&data) { match msg_in.union { @@ -1290,13 +1307,16 @@ impl Remote { return false; } } + if self.handler.is_terminal() { + if !self.check_terminal_support(&peer_version) { + self.handler.lc.write().unwrap().handle_peer_info(&pi); + return false; + } + } self.handler.handle_peer_info(pi); #[cfg(all(target_os = "windows", not(feature = "flutter")))] self.check_clipboard_file_context(); - if !(self.handler.is_file_transfer() - || self.handler.is_port_forward() - || self.handler.is_view_camera()) - { + if self.handler.is_default() { #[cfg(feature = "flutter")] #[cfg(not(target_os = "ios"))] let rx = Client::try_start_clipboard(None); @@ -1661,9 +1681,6 @@ impl Remote { ); } } - Ok(Permission::Camera) => { - self.handler.set_permission("camera", p.enabled); - } Ok(Permission::Restart) => { self.handler.set_permission("restart", p.enabled); } @@ -1923,6 +1940,18 @@ impl Remote { self.handler .handle_screenshot_resp(response.sid, response.msg); } + Some(message::Union::TerminalResponse(response)) => { + use hbb_common::message_proto::terminal_response::Union; + if let Some(Union::Opened(opened)) = &response.union { + if opened.success && !opened.service_id.is_empty() { + self.handler.lc.write().unwrap().set_option( + "terminal-service-id".to_owned(), + opened.service_id.clone(), + ); + } + } + self.handler.handle_terminal_response(response); + } _ => {} } } @@ -1931,6 +1960,12 @@ impl Remote { fn set_peer_info(&mut self, pi: &PeerInfo) { self.peer_info.platform = pi.platform.clone(); + + // Check features field for terminal support + if let Some(features) = pi.features.as_ref() { + self.peer_info.support_terminal = features.terminal; + } + if let Ok(platform_additions) = serde_json::from_str::>(&pi.platform_additions) { diff --git a/src/flutter.rs b/src/flutter.rs index 298ba419b0d..e3c3c8c0daf 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1105,6 +1105,60 @@ impl InvokeUiSession for FlutterHandler { } } } + + fn handle_terminal_response(&self, response: TerminalResponse) { + use hbb_common::message_proto::terminal_response::Union; + + match response.union { + Some(Union::Opened(opened)) => { + let mut event_data: Vec<(&str, serde_json::Value)> = vec![ + ("type", json!("opened")), + ("terminal_id", json!(opened.terminal_id)), + ("success", json!(opened.success)), + ("message", json!(&opened.message)), + ("pid", json!(opened.pid)), + ("service_id", json!(&opened.service_id)), + ]; + self.push_event_("terminal_response", &event_data, &[], &[]); + } + Some(Union::Data(data)) => { + // Decompress data if needed + let output_data = if data.compressed { + hbb_common::compress::decompress(&data.data) + } else { + data.data.to_vec() + }; + + let encoded = crate::encode64(&output_data); + let event_data: Vec<(&str, serde_json::Value)> = vec![ + ("type", json!("data")), + ("terminal_id", json!(data.terminal_id)), + ("data", json!(&encoded)), + ]; + self.push_event_("terminal_response", &event_data, &[], &[]); + } + Some(Union::Closed(closed)) => { + let event_data: Vec<(&str, serde_json::Value)> = vec![ + ("type", json!("closed")), + ("terminal_id", json!(closed.terminal_id)), + ("exit_code", json!(closed.exit_code)), + ]; + self.push_event_("terminal_response", &event_data, &[], &[]); + } + Some(Union::Error(error)) => { + let event_data: Vec<(&str, serde_json::Value)> = vec![ + ("type", json!("error")), + ("terminal_id", json!(error.terminal_id)), + ("message", json!(&error.message)), + ]; + self.push_event_("terminal_response", &event_data, &[], &[]); + } + None => {} + Some(_) => { + log::warn!("Unhandled terminal response type"); + } + } + } } impl FlutterHandler { @@ -1221,6 +1275,7 @@ pub fn session_add( is_view_camera: bool, is_port_forward: bool, is_rdp: bool, + is_terminal: bool, switch_uuid: &str, force_relay: bool, password: String, @@ -1231,6 +1286,8 @@ pub fn session_add( ConnType::FILE_TRANSFER } else if is_view_camera { ConnType::VIEW_CAMERA + } else if is_terminal { + ConnType::TERMINAL } else if is_port_forward { if is_rdp { ConnType::RDP diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d3665d7cc42..5a6b66a0b6d 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -103,6 +103,8 @@ pub fn peer_get_sessions_count(id: String, conn_type: i32) -> SyncReturn ConnType::PORT_FORWARD } else if conn_type == ConnType::RDP as i32 { ConnType::RDP + } else if conn_type == ConnType::TERMINAL as i32 { + ConnType::TERMINAL } else { ConnType::DEFAULT_CONN }; @@ -129,6 +131,7 @@ pub fn session_add_sync( is_view_camera: bool, is_port_forward: bool, is_rdp: bool, + is_terminal: bool, switch_uuid: String, force_relay: bool, password: String, @@ -142,6 +145,7 @@ pub fn session_add_sync( is_view_camera, is_port_forward, is_rdp, + is_terminal, &switch_uuid, force_relay, password, @@ -613,6 +617,33 @@ pub fn session_send_chat(session_id: SessionID, text: String) { } } +// Terminal functions +pub fn session_open_terminal(session_id: SessionID, terminal_id: i32, rows: u32, cols: u32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.open_terminal(terminal_id, rows, cols); + } else { + log::error!("[flutter_ffi] Session not found for session_id: {}", session_id); + } +} + +pub fn session_send_terminal_input(session_id: SessionID, terminal_id: i32, data: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.send_terminal_input(terminal_id, data); + } +} + +pub fn session_resize_terminal(session_id: SessionID, terminal_id: i32, rows: u32, cols: u32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.resize_terminal(terminal_id, rows, cols); + } +} + +pub fn session_close_terminal(session_id: SessionID, terminal_id: i32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.close_terminal(terminal_id); + } +} + pub fn session_peer_option(session_id: SessionID, name: String, value: String) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.set_option(name, value); diff --git a/src/ipc.rs b/src/ipc.rs index a74a0c1034a..0c6a8d57e8c 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -190,6 +190,7 @@ pub enum Data { id: i32, is_file_transfer: bool, is_view_camera: bool, + is_terminal: bool, peer_id: String, name: String, authorized: bool, diff --git a/src/lang/ar.rs b/src/lang/ar.rs index e03d0e2823b..bff726e3b97 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "لا يوجد اذن نقل الملف"), ("Note", "ملاحظة"), ("Connection", "الاتصال"), - ("Share Screen", "مشاركة الشاشة"), + ("Share screen", "مشاركة الشاشة"), ("Chat", "محادثة"), ("Total", "الاجمالي"), ("items", "عناصر"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "لقط الشاشة"), ("Input Control", "تحكم الادخال"), ("Audio Capture", "لقط الصوت"), - ("File Connection", "اتصال الملف"), - ("Screen Connection", "اتصال الشاشة"), ("Do you accept?", "هل تقبل؟"), ("Open System Setting", "فتح اعدادات النظام"), ("How to get Android input permission?", "كيف تحصل على اذن الادخال في اندرويد؟"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "غير موسوم"), ("new-version-of-{}-tip", "تحديث جديد متاح لـ {}"), ("Accessible devices", "الأجهزة القابلة للوصول"), - ("View camera", "عرض الكاميرا"), ("upgrade_remote_rustdesk_client_to_{}_tip", "ترقية عميل RustDesk البعيد إلى {}"), ("view_camera_unsupported_tip", "عرض الكاميرا غير مدعوم في هذا الجهاز"), - ("Enable camera", "تمكين الكاميرا"), - ("No cameras", "لا توجد كاميرات"), ("d3d_render_tip", "تمكين العرض باستخدام D3D"), ("Use D3D rendering", "استخدام عرض D3D"), ("Printer", "الطابعة"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "كلمة مرور رقمية لمرة واحدة"), ("Enable IPv6 P2P connection", "تمكين اتصال نظير إلى نظير عبر IPv6"), ("Enable UDP hole punching", "تمكين تقنية حفر الثغرات عبر UDP"), + ("View camera", "عرض الكاميرا"), + ("Enable camera", "تمكين الكاميرا"), + ("No cameras", "لا توجد كاميرات"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 3f3a9310de4..79eb530c413 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Няма дазволу на перадачу файлаў"), ("Note", "Нататка"), ("Connection", "Падключэнне"), - ("Share Screen", "Дзяліцца экранам"), + ("Share screen", "Дзяліцца экранам"), ("Chat", "Чат"), ("Total", "Усяго"), ("items", "элементы"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Захоп экрана"), ("Input Control", "Кіраванне ўводам"), ("Audio Capture", "Захоп аўдыё"), - ("File Connection", "Падлучэнне перадачы файлаў"), - ("Screen Connection", "Падлучэнне прагляду/кіравання экранам"), ("Do you accept?", "Ці вы згодны?"), ("Open System Setting", "Адкрыць налады сістэмы"), ("How to get Android input permission?", "Як атрымаць дазвол на ўвод Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Прагляд камеры"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Калі ласка, абнавіце кліент RustDesk да версіі {} або навейшай на аддаленым баку!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Прагляд камеры"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 7b479481f18..1d90a83e211 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Няма разрешение за прехвърляне на файлове"), ("Note", "Бележка"), ("Connection", "Връзка"), - ("Share Screen", "Сподели екран"), + ("Share screen", "Сподели екран"), ("Chat", "Чат"), ("Total", "Общо"), ("items", "неща"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Заснемане на екрана"), ("Input Control", "Управление на въвеждане"), ("Audio Capture", "Аудиозапис"), - ("File Connection", "Файлова връзка"), - ("Screen Connection", "Екранна връзка"), ("Do you accept?", "Приемате ли?"), ("Open System Setting", "Отворете системните настройки"), ("How to get Android input permission?", "Как да получим право за въвеждане при Андроид?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Преглед на камерата"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Моля, надстройте клиента RustDesk до версия {} или по-нова от отдалечената страна!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Преглед на камерата"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index bd15d2c480b..c4d88fa68d7 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Cap permís per a transferència de fitxers"), ("Note", "Nota"), ("Connection", "Connexió"), - ("Share Screen", "Compartició de pantalla"), + ("Share screen", "Compartició de pantalla"), ("Chat", "Xat"), ("Total", "Total"), ("items", "elements"), @@ -275,8 +275,6 @@ 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ó de fitxer"), - ("Screen Connection", "Connexió de pantalla"), ("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?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sense etiquetar"), ("new-version-of-{}-tip", ""), ("Accessible devices", "Dispositius accessibles"), - ("View camera", "Mostra la càmera"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre à niveau le client RustDesk vers la version {} ou plus récente du côté distant !"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Mostra la càmera"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index a4f7d09d2b4..974d2a99268 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "没有文件传输权限"), ("Note", "备注"), ("Connection", "连接"), - ("Share Screen", "共享屏幕"), + ("Share screen", "共享屏幕"), ("Chat", "聊天消息"), ("Total", "总计"), ("items", "个项目"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "屏幕录制"), ("Input Control", "输入控制"), ("Audio Capture", "音频录制"), - ("File Connection", "文件连接"), - ("Screen Connection", "屏幕连接"), ("Do you accept?", "是否接受?"), ("Open System Setting", "打开系统设置"), ("How to get Android input permission?", "如何获取安卓的输入权限?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "无标签"), ("new-version-of-{}-tip", "{} 版本更新"), ("Accessible devices", "可访问的设备"), - ("View camera", "查看摄像头"), ("upgrade_remote_rustdesk_client_to_{}_tip", "请在远程端将 RustDesk 客户端升级至版本 {} 或更新版本!"), ("view_camera_unsupported_tip", "您的远程端不支持查看摄像头。"), - ("Enable camera", "允许查看摄像头"), - ("No cameras", "没有摄像头"), ("d3d_render_tip", "当启用 D3D 渲染时,某些机器可能无法显示远程画面。"), ("Use D3D rendering", "使用 D3D 渲染"), ("Printer", "打印机"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "一次性密码为数字"), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "查看摄像头"), + ("Enable camera", "允许查看摄像头"), + ("No cameras", "没有摄像头"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index b4a903b8611..2b99704b329 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Žádné oprávnění k přenosu souborů"), ("Note", "Poznámka"), ("Connection", "Připojení"), - ("Share Screen", "Sdílet obrazovku"), + ("Share screen", "Sdílet obrazovku"), ("Chat", "Chat"), ("Total", "Celkem"), ("items", "Položek"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Zachytávání obrazovky"), ("Input Control", "Ovládání vstupních zařízení"), ("Audio Capture", "Zachytávání zvuku"), - ("File Connection", "Souborové spojení"), - ("Screen Connection", "Spojení obrazovky"), ("Do you accept?", "Přijímáte?"), ("Open System Setting", "Otevřít nastavení systému"), ("How to get Android input permission?", "Jak v systému Android získat oprávnění pro vstupní zařízení?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Zobrazit kameru"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgradujte prosím klienta RustDesk na verzi {} nebo novější na vzdálené straně!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Zobrazit kameru"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 2bd61727653..52641769b6a 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Ingen tilladelse til at overføre filen"), ("Note", "Note"), ("Connection", "Forbindelse"), - ("Share Screen", "Del skærmen"), + ("Share screen", "Del skærmen"), ("Chat", "Chat"), ("Total", "Total"), ("items", "artikel"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Skærmoptagelse"), ("Input Control", "Inputkontrol"), ("Audio Capture", "Lydoptagelse"), - ("File Connection", "Filforbindelse"), - ("Screen Connection", "Færdiggørelse"), ("Do you accept?", "Accepterer du?"), ("Open System Setting", "Åbn systemindstillingen"), ("How to get Android input permission?", "Hvordan får jeg en Android-input tilladelse?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Se kamera"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Opgrader venligst RustDesk-klienten til version {} eller nyere på fjernsiden!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Se kamera"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index d7611af0c50..0db57aa7a81 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Keine Berechtigung für die Dateiübertragung"), ("Note", "Hinweis"), ("Connection", "Verbindung"), - ("Share Screen", "Bildschirm freigeben"), + ("Share screen", "Bildschirm freigeben"), ("Chat", "Chat"), ("Total", "Gesamt"), ("items", "Einträge"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Bildschirmaufnahme"), ("Input Control", "Eingabesteuerung"), ("Audio Capture", "Audioaufnahme"), - ("File Connection", "Dateiverbindung"), - ("Screen Connection", "Bildschirmverbindung"), ("Do you accept?", "Verbindung zulassen?"), ("Open System Setting", "Systemeinstellung öffnen"), ("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Unmarkiert"), ("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"), ("Accessible devices", "Erreichbare Geräte"), - ("View camera", "Kamera anzeigen"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Bitte aktualisieren Sie den RustDesk-Client auf der Remote-Seite auf Version {} oder neuer!"), ("view_camera_unsupported_tip", "Das entfernte Gerät kann die Kamera nicht anzeigen."), - ("Enable camera", "Kamera zulassen"), - ("No cameras", "Keine Kameras"), ("d3d_render_tip", "Wenn das D3D-Rendering aktiviert ist, kann der entfernte Bildschirm auf manchen Rechnern schwarz sein."), ("Use D3D rendering", "D3D-Rendering verwenden"), ("Printer", "Drucker"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "Numerisches Einmalpasswort"), ("Enable IPv6 P2P connection", "IPv6-P2P-Verbindung aktivieren"), ("Enable UDP hole punching", "UDP-Hole-Punching aktivieren"), + ("View camera", "Kamera anzeigen"), + ("Enable camera", "Kamera zulassen"), + ("No cameras", "Keine Kameras"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 46458559336..9f6bcee468d 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Δεν υπάρχει άδεια για μεταφορά αρχείων"), ("Note", "Σημείωση"), ("Connection", "Σύνδεση"), - ("Share Screen", "Κοινή χρήση οθόνης"), + ("Share screen", "Κοινή χρήση οθόνης"), ("Chat", "Κουβέντα"), ("Total", "Σύνολο"), ("items", "στοιχεία"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Αποτύπωση οθόνης"), ("Input Control", "Έλεγχος εισόδου"), ("Audio Capture", "Εγγραφή ήχου"), - ("File Connection", "Σύνδεση αρχείου"), - ("Screen Connection", "Σύνδεση οθόνης"), ("Do you accept?", "Δέχεσαι;"), ("Open System Setting", "Άνοιγμα ρυθμίσεων συστήματος"), ("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισαγωγής Android;"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Χωρίς ετικέτα"), ("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"), ("Accessible devices", "Προσβάσιμες συσκευές"), - ("View camera", "Προβολή κάμερας"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότερη στην απομακρυσμένη πλευρά!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Προβολή κάμερας"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index d0ee84239e6..4570c832422 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -77,12 +77,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Canvas Move", "Canvas move"), ("Pinch to Zoom", "Pinch to zoom"), ("Canvas Zoom", "Canvas zoom"), - ("Share Screen", "Share screen"), ("Screen Capture", "Screen capture"), ("Input Control", "Input control"), ("Audio Capture", "Audio capture"), - ("File Connection", "File connection"), - ("Screen Connection", "Screen connection"), ("Open System Setting", "Open system setting"), ("android_input_permission_tip1", "In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the \"Accessibility\" service."), ("android_input_permission_tip2", "Please go to the next system settings page, find and enter [Installed Services], turn on [RustDesk Input] service."), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 25e2a5ec71b..b454dc49278 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Neniu permeso de dosiertransigo"), ("Note", "Notu"), ("Connection", "Konekto"), - ("Share Screen", "Kunhavigi Ekranon"), + ("Share screen", "Kunhavigi Ekranon"), ("Chat", "Babilo"), ("Total", "Sumo"), ("items", "eroj"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Rigardi kameron"), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Rigardi kameron"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 847bfd447b0..998c278cf30 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Sin permiso de transferencia de archivos"), ("Note", "Nota"), ("Connection", "Conexión"), - ("Share Screen", "Compartir pantalla"), + ("Share screen", "Compartir pantalla"), ("Chat", "Chat"), ("Total", "Total"), ("items", "items"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Captura de pantalla"), ("Input Control", "Control de entrada"), ("Audio Capture", "Captura de audio"), - ("File Connection", "Conexión de archivos"), - ("Screen Connection", "Conexión de pantalla"), ("Do you accept?", "¿Aceptas?"), ("Open System Setting", "Configuración del sistema abierto"), ("How to get Android input permission?", "¿Cómo obtener el permiso de entrada de Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sin itiquetar"), ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), ("Accessible devices", ""), - ("View camera", "Ver cámara"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"), ("view_camera_unsupported_tip", "El dispositivo remoto no soporta la visualización de la cámara."), - ("Enable camera", "Habilitar cámara"), - ("No cameras", "No hay cámaras"), ("d3d_render_tip", "Al activar el renderizado D3D, la pantalla de control remoto puede verse negra en algunos equipos."), ("Use D3D rendering", "Usar renderizado D3D"), ("Printer", "Impresora"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Ver cámara"), + ("Enable camera", "Habilitar cámara"), + ("No cameras", "No hay cámaras"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index eff5c6d9e34..7928ebe11da 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Failiülekande luba puudub"), ("Note", "Märkus"), ("Connection", "Ühendus"), - ("Share Screen", "Jaga ekraani"), + ("Share screen", "Jaga ekraani"), ("Chat", "Vestlus"), ("Total", "Kokku"), ("items", "üksust"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Ekraanisalvestus"), ("Input Control", "Sisendjuhtimine"), ("Audio Capture", "Helisalvestus"), - ("File Connection", "Failiühendus"), - ("Screen Connection", "Kuvaühendus"), ("Do you accept?", "Kas nõustud?"), ("Open System Setting", "Ava süsteemisätted"), ("How to get Android input permission?", "Kuidas saada Androidi sisendi luba?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sildistamata"), ("new-version-of-{}-tip", "Saadaval on {} uus versioon"), ("Accessible devices", "Ligipääsetavad seadmed"), - ("View camera", "Vaata kaamerat"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Täiendage RustDeski klient kaugküljel versioonile {} või uuemale!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Vaata kaamerat"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 24eac04601c..ab59dfc24c6 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Ez duzu baimenik fitxategiak transferitzeko"), ("Note", "Nota"), ("Connection", "Konexioa"), - ("Share Screen", "Partekatu pantaila"), + ("Share screen", "Partekatu pantaila"), ("Chat", "Txata"), ("Total", "Guztira"), ("items", "elementuak"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Pantaila-grabazioa"), ("Input Control", "Sarrera-kontrola"), ("Audio Capture", "Audio-grabazioa"), - ("File Connection", "Fitxategi-konexioa"), - ("Screen Connection", "Pantaila-konexioa"), ("Do you accept?", "Onartzen al duzu?"), ("Open System Setting", "Ireki sistemaren ezarpenak"), ("How to get Android input permission?", "Nola lortu dezaket Android sarrera-baimena?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Ikusi kamera"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Mesedez, eguneratu RustDesk bezeroa {} bertsiora edo berriagoa urruneko aldean!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Ikusi kamera"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 2dacb9dd7de..b4fef049bd8 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "مجوز انتقال فایل داده نشده"), ("Note", "یادداشت"), ("Connection", "ارتباط"), - ("Share Screen", "اشتراک گذاری صفحه"), + ("Share screen", "اشتراک گذاری صفحه"), ("Chat", "چت"), ("Total", "مجموع"), ("items", "آیتم ها"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "ضبط صفحه"), ("Input Control", "کنترل ورودی"), ("Audio Capture", "ضبط صدا"), - ("File Connection", "ارتباط فایل"), - ("Screen Connection", "ارتباط صفحه"), ("Do you accept?", "آیا می پذیرید؟"), ("Open System Setting", "باز کردن تنظیمات سیستم"), ("How to get Android input permission?", "چگونه مجوز ورود به سیستم اندروید را دریافت کنیم؟"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "بدون برچسب"), ("new-version-of-{}-tip", "نسخه جدید {} در دسترس است"), ("Accessible devices", "دستگاه‌های در دسترس"), - ("View camera", "نمایش دوربین"), ("upgrade_remote_rustdesk_client_to_{}_tip", "لطفاً RustDesk را به نسخه {} یا جدیدتر در سمت راه دور ارتقا دهید"), ("view_camera_unsupported_tip", "دوربین در این دستگاه پشتیبانی نمی‌شود"), - ("Enable camera", "فعال کردن دوربین"), - ("No cameras", "هیچ دوربینی یافت نشد"), ("d3d_render_tip", "فعال کردن رندر D3D برای عملکرد بهتر"), ("Use D3D rendering", "استفاده از رندر D3D"), ("Printer", "چاپگر"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "رمز عبور یک‌بار مصرف عددی"), ("Enable IPv6 P2P connection", "فعال‌سازی اتصال همتا‌به‌همتای IPv6"), ("Enable UDP hole punching", "فعال‌سازی تکنیک UDP hole punching"), + ("View camera", "نمایش دوربین"), + ("Enable camera", "فعال کردن دوربین"), + ("No cameras", "هیچ دوربینی یافت نشد"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index ce4af5ac11c..6e0b4f4c76e 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Absence de l’autorisation de transfert de fichiers"), ("Note", "Note"), ("Connection", "Connexion"), - ("Share Screen", "Partage d’écran"), + ("Share screen", "Partage d’écran"), ("Chat", "Discussion"), ("Total", "Total"), ("items", "éléments"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Capture de l’écran"), ("Input Control", "Contrôle de la saisie"), ("Audio Capture", "Capture de l’audio"), - ("File Connection", "Connexion aux fichiers"), - ("Screen Connection", "Connexion à l’écran"), ("Do you accept?", "Acceptez-vous ?"), ("Open System Setting", "Ouvrir les paramètres système"), ("How to get Android input permission?", "Comment obtenir l’autorisation de contrôle de la saisie sur Android ?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sans étiquette"), ("new-version-of-{}-tip", "Une nouvelle version de {} est disponible"), ("Accessible devices", "Appareils accessibles"), - ("View camera", "Afficher la caméra"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre le client RustDesk distant à jour vers la version {} ou ultérieure !"), ("view_camera_unsupported_tip", "L’appareil distant ne prend pas en charge l’affichage de la caméra."), - ("Enable camera", "Activer la caméra"), - ("No cameras", "Aucune caméra"), ("d3d_render_tip", "Sur certaines machines, l’écran du contrôle à distance peut rester noir lors de l’utilisation du rendu D3D."), ("Use D3D rendering", "Utiliser le rendu D3D"), ("Printer", "Imprimante"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "Mot de passe à usage unique numérique"), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Afficher la caméra"), + ("Enable camera", "Activer la caméra"), + ("No cameras", "Aucune caméra"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index d8668c27ff2..9d83ff58a7d 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "ფაილების გადაცემის უფლება არ არის"), ("Note", "შენიშვნა"), ("Connection", "კავშირი"), - ("Share Screen", "ეკრანის დემონსტრაცია"), + ("Share screen", "ეკრანის დემონსტრაცია"), ("Chat", "ჩატი"), ("Total", "სულ"), ("items", "ელემენტები"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "ეკრანის ჩაწერა"), ("Input Control", "შეყვანის კონტროლი"), ("Audio Capture", "აუდიოს ჩაწერა"), - ("File Connection", "ფაილების გადაცემის დაკავშირება"), - ("Screen Connection", "ეკრანის ნახვის/მართვის დაკავშირება"), ("Do you accept?", "თანახმა ხართ?"), ("Open System Setting", "სისტემის პარამეტრების გახსნა"), ("How to get Android input permission?", "როგორ მივიღოთ Android-ის შეყვანის უფლება?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "უტეგო"), ("new-version-of-{}-tip", "ხელმისაწვდომია ახალი ვერსია {}"), ("Accessible devices", "ხელმისაწვდომი მოწყობილობები"), - ("View camera", "კამერის ნახვა"), ("upgrade_remote_rustdesk_client_to_{}_tip", "განაახლეთ RustDesk კლიენტი ვერსიამდე {} ან უფრო ახალი დისტანციურ მხარეზე!"), ("view_camera_unsupported_tip", "დისტანციური მოწყობილობა არ უჭერს მხარს კამერის ნახვას."), - ("Enable camera", "კამერის ჩართვა"), - ("No cameras", "კამერა არ არის"), ("d3d_render_tip", "D3D ვიზუალიზაციის ჩართვისას ზოგიერთ მოწყობილობაზე დისტანციური ეკრანი შეიძლება იყოს შავი."), ("Use D3D rendering", "D3D ვიზუალიზაციის გამოყენება"), ("Printer", "პრინტერი"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "კამერის ნახვა"), + ("Enable camera", "კამერის ჩართვა"), + ("No cameras", "კამერა არ არის"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 3f051cf8bd8..fc55dd94d2d 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "אין הרשאת העברת קבצים"), ("Note", "הערה"), ("Connection", "התחברות"), - ("Share Screen", "שיתוף מסך"), + ("Share screen", "שיתוף מסך"), ("Chat", "צ'אט"), ("Total", "הכל"), ("items", "פריטים"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "לכידת מסך"), ("Input Control", "בקרת קלט"), ("Audio Capture", "לכידת שמע"), - ("File Connection", "חיבור להעברת קבצים"), - ("Screen Connection", "חיבור תצוגה"), ("Do you accept?", "האם אתה מקבל?"), ("Open System Setting", "פתח הגדרות מערכת"), ("How to get Android input permission?", "כיצד לקבל הרשאת קלט באנדרואיד?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "לא מתוייג"), ("new-version-of-{}-tip", "גרסה חדשה של {} זמינה"), ("Accessible devices", "מכשירים נגישים"), - ("View camera", "הצג מצלמה"), ("upgrade_remote_rustdesk_client_to_{}_tip", "אנא שדרג את לקוח RustDesk לגרסה {} או חדשה יותר בצד המרוחק!"), ("view_camera_unsupported_tip", "הצגת מצלמה אינה נתמכת במכשיר המרוחק"), - ("Enable camera", "הפעל מצלמה"), - ("No cameras", "אין מצלמות"), ("d3d_render_tip", "שימוש בעיבוד Direct3D עשוי לשפר ביצועים בחלק מהמקרים"), ("Use D3D rendering", "השתמש בעיבוד D3D"), ("Printer", "מדפסת"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "הצג מצלמה"), + ("Enable camera", "הפעל מצלמה"), + ("No cameras", "אין מצלמות"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 9dd074b26c5..a0ea613c1d5 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nemate pravo prijenosa datoteka"), ("Note", "Bilješka"), ("Connection", "Povezivanje"), - ("Share Screen", "Podijeli zaslon"), + ("Share screen", "Podijeli zaslon"), ("Chat", "Dopisivanje"), ("Total", "Ukupno"), ("items", "stavki"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Snimanje zaslona"), ("Input Control", "Kontrola unosa"), ("Audio Capture", "Snimanje zvuka"), - ("File Connection", "Spajanje preko datoteke"), - ("Screen Connection", "Podijelite vezu"), ("Do you accept?", "Prihvaćate li?"), ("Open System Setting", "Postavke otvorenog sustava"), ("How to get Android input permission?", "Kako dobiti pristup za unos na Androidu?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Pregled kamere"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Molimo ažurirajte RustDesk klijent na verziju {} ili noviju na udaljenoj strani!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Pregled kamere"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 4c356c0d147..f4bb24734bc 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nincs engedély a fájlátvitelre"), ("Note", "Megjegyzés"), ("Connection", "Kapcsolat"), - ("Share Screen", "Képernyőmegosztás"), + ("Share screen", "Képernyőmegosztás"), ("Chat", "Csevegés"), ("Total", "Összes"), ("items", "elemek"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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?", "Elfogadás?"), ("Open System Setting", "Rendszerbeállítások megnyitása"), ("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Címkézetlen"), ("new-version-of-{}-tip", "A(z) {} új verziója"), ("Accessible devices", "Hozzáférhető eszközök"), - ("View camera", "Kamera megtekintése"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Frissítse a RustDesk klienst {} vagy újabb verziójára a távoli oldalon!"), ("view_camera_unsupported_tip", "A kameranézet nem támogatott"), - ("Enable camera", "Kamera engedélyezése"), - ("No cameras", "Nincs kamera"), ("d3d_render_tip", "D3D renderelés"), ("Use D3D rendering", "D3D renderelés használata"), ("Printer", "Nyomtató"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Kamera megtekintése"), + ("Enable camera", "Kamera engedélyezése"), + ("No cameras", "Nincs kamera"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 245a1562ae0..8d287dda197 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Tidak ada izin untuk mengirim file"), ("Note", "Catatan"), ("Connection", "Koneksi"), - ("Share Screen", "Bagikan Layar"), + ("Share screen", "Bagikan Layar"), ("Chat", "Obrolan"), ("Total", "Total"), ("items", "item"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Tangkapan Layar"), ("Input Control", "Kontrol input"), ("Audio Capture", "Rekam Suara"), - ("File Connection", "Koneksi File"), - ("Screen Connection", "Koneksi layar"), ("Do you accept?", "Apakah anda setuju?"), ("Open System Setting", "Buka Pengaturan Sistem"), ("How to get Android input permission?", "Bagaimana cara mendapatkan izin input dari Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", "Versi {} sudah tersedia."), ("Accessible devices", "Perangkat yang tersedia"), - ("View camera", "Lihat Kamera"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Silahkan perbarui aplikasi RustDesk ke versi {} atau yang lebih baru pada komputer yang akan terhubung!"), ("view_camera_unsupported_tip", "Perangkat yang terhubung tidak mendukung tampilan kamera."), - ("Enable camera", "Aktifkan kamera"), - ("No cameras", "Tidak ada kamera"), ("d3d_render_tip", "Ketika rendering D3D diaktifkan, layar kontrol jarak jauh bisa tampak hitam di beberapa komputer"), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Lihat Kamera"), + ("Enable camera", "Aktifkan kamera"), + ("No cameras", "Tidak ada kamera"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 77addcf64df..16c2097c8b2 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nessun permesso per il trasferimento file"), ("Note", "Nota"), ("Connection", "Connessione"), - ("Share Screen", "Condividi schermo"), + ("Share screen", "Condividi schermo"), ("Chat", "Chat"), ("Total", "Totale"), ("items", "Oggetti"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Cattura schermo"), ("Input Control", "Controllo input"), ("Audio Capture", "Acquisizione audio"), - ("File Connection", "Connessione file"), - ("Screen Connection", "Connessione schermo"), ("Do you accept?", "Accetti?"), ("Open System Setting", "Apri impostazioni di sistema"), ("How to get Android input permission?", "Come ottenere l'autorizzazione input in Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Senza tag"), ("new-version-of-{}-tip", "È disponibile una nuova versione di {}"), ("Accessible devices", "Dispositivi accessibili"), - ("View camera", "Visualizza telecamera"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Aggiorna il client RustDesk remoto alla versione {} o successiva!"), ("view_camera_unsupported_tip", "Il dispositivo remoto non supporta la visualizzazione della camera."), - ("Enable camera", "Abilita camera"), - ("No cameras", "Nessuna camera"), ("d3d_render_tip", "Quando è abilitato il rendering D3D, in alcuni computer la schermata del telecomando potrebbe essere nera."), ("Use D3D rendering", "Usa rendering D3D"), ("Printer", "Stampante"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "Password numerica monouso"), ("Enable IPv6 P2P connection", "Abilita connessione P2P IPv6"), ("Enable UDP hole punching", "Abilita hole punching UDP"), + ("View camera", "Visualizza telecamera"), + ("Enable camera", "Abilita camera"), + ("No cameras", "Nessuna camera"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index b2c57d06e5f..efa547095cc 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "ファイル転送の権限がありません"), ("Note", "ノート"), ("Connection", "接続"), - ("Share Screen", "画面を共有"), + ("Share screen", "画面を共有"), ("Chat", "チャット"), ("Total", "計"), ("items", "個のアイテム"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "画面キャプチャ"), ("Input Control", "入力操作"), ("Audio Capture", "音声キャプチャ"), - ("File Connection", "ファイルの接続"), - ("Screen Connection", "画面の接続"), ("Do you accept?", "許可しますか?"), ("Open System Setting", "システム設定を開く"), ("How to get Android input permission?", "Androidの入力権限を取得するには?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "カメラを表示"), ("upgrade_remote_rustdesk_client_to_{}_tip", "リモート側のRustDeskクライアントをバージョン{}以上にアップグレードしてください!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "カメラを表示"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 195f837217d..8db1e737c7d 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "파일 전송 권한이 없습니다."), ("Note", "메모"), ("Connection", "연결"), - ("Share Screen", "화면 공유"), + ("Share screen", "화면 공유"), ("Chat", "채팅"), ("Total", "총"), ("items", "개"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "화면 캡처"), ("Input Control", "입력 제어"), ("Audio Capture", "오디오 캡처"), - ("File Connection", "파일 전송"), - ("Screen Connection", "화면 전송"), ("Do you accept?", "수락하시겠습니까?"), ("Open System Setting", "시스템 설정 열기"), ("How to get Android input permission?", "Android 입력 권한을 얻는 방법"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "태그 없음"), ("new-version-of-{}-tip", "{}의 새 버전이 출시되었습니다."), ("Accessible devices", "연결 가능한 기기"), - ("View camera", "카메라 보기"), ("upgrade_remote_rustdesk_client_to_{}_tip", "원격 기기의 RustDesk 클라이언트를 {} 버전 이상으로 업그레이드하십시오!"), ("view_camera_unsupported_tip", "원격 기기에서 카메라 보기를 지원하지 않습니다."), - ("Enable camera", "카메라 보기 허용"), - ("No cameras", "카메라 없음"), ("d3d_render_tip", "D3D 렌더링을 활성화하면 일부 기기에서 원격 화면이 표시되지 않을 수 있습니다."), ("Use D3D rendering", "D3D 렌더링 활성화"), ("Printer", "프린터"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "일회용 비밀번호"), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "카메라 보기"), + ("Enable camera", "카메라 보기 허용"), + ("No cameras", "카메라 없음"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 19d4c00625b..79b94db1c90 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Файыл алмасуға рұқсат берілмеген"), ("Note", "Нота"), ("Connection", "Қосылым"), - ("Share Screen", "Екіренді Бөлісу"), + ("Share screen", "Екіренді Бөлісу"), ("Chat", "Чат"), ("Total", "Барлығы"), ("items", "зат"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Екіренді Түсіру"), ("Input Control", "Еңгізуді Басқару/Қадағалау"), ("Audio Capture", "Аудио Түсіру"), - ("File Connection", "Файыл Қосылымы"), - ("Screen Connection", "Екірен Қосылымы"), ("Do you accept?", "Қабылдайсыз ба?"), ("Open System Setting", "Жүйе Орнатпаларын Ашу"), ("How to get Android input permission?", "Android еңгізу рұқсатын қалай алуға болады?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Камераны Көру"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Қашықтағы жақтағы RustDesk клиентін {} немесе одан жоғары нұсқаға жаңартуды өтінеміз!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Камераны Көру"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 37adeb2ac1d..be0f69345fa 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nėra leidimo perkelti failus"), ("Note", "Pastaba"), ("Connection", "Ryšys"), - ("Share Screen", "Bendrinti ekraną"), + ("Share screen", "Bendrinti ekraną"), ("Chat", "Pokalbis"), ("Total", "Iš viso"), ("items", "elementai"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Ekrano nuotrauka"), ("Input Control", "Įvesties valdymas"), ("Audio Capture", "Garso fiksavimas"), - ("File Connection", "Failo ryšys"), - ("Screen Connection", "Ekrano jungtis"), ("Do you accept?", "Ar sutinki?"), ("Open System Setting", "Atviros sistemos nustatymas"), ("How to get Android input permission?", "Kaip gauti Android įvesties leidimą?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Peržiūrėti kamerą"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Prašome atnaujinti nuotolinės pusės RustDesk klientą į {} ar naujesnę versiją!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Peržiūrėti kamerą"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 46ae0e39714..28766130037 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nav atļaujas failu pārsūtīšanai"), ("Note", "Piezīme"), ("Connection", "Savienojums"), - ("Share Screen", "Koplietot ekrānu"), + ("Share screen", "Koplietot ekrānu"), ("Chat", "Tērzēšana"), ("Total", "Kopā"), ("items", "vienumi"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Ekrāna tveršana"), ("Input Control", "Ievades vadība"), ("Audio Capture", "Audio tveršana"), - ("File Connection", "Failu savienojums"), - ("Screen Connection", "Ekrāna savienojums"), ("Do you accept?", "Vai Jūs pieņemat?"), ("Open System Setting", "Atvērt sistēmas iestatījumus"), ("How to get Android input permission?", "Kā iegūt Android ievades atļauju?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Neatzīmēts"), ("new-version-of-{}-tip", "Ir pieejama jauna {} versija"), ("Accessible devices", "Pieejamas ierīces"), - ("View camera", "Skatīt kameru"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Lūdzu, jauniniet attālās puses RustDesk klientu uz versiju {} vai jaunāku!"), ("view_camera_unsupported_tip", "Attālā ierīce neatbalsta kameras skatīšanos."), - ("Enable camera", "Iespējot kameru"), - ("No cameras", "Nav kameru"), ("d3d_render_tip", "Ja ir iespējota D3D renderēšana, dažās ierīcēs tālvadības pults ekrāns var būt melns."), ("Use D3D rendering", "Izmantot D3D renderēšanu"), ("Printer", "Printeris"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "Vienreiz lietojama ciparu parole"), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Skatīt kameru"), + ("Enable camera", "Iespējot kameru"), + ("No cameras", "Nav kameru"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 4a646251c5f..47aad365286 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Ingen tillatelse til å overføre filen"), ("Note", "Notat"), ("Connection", "Tilkobling"), - ("Share Screen", "Del skjermen"), + ("Share screen", "Del skjermen"), ("Chat", "Chat"), ("Total", "Total"), ("items", "Objekter"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Skjermopptak"), ("Input Control", "Input kontroll"), ("Audio Capture", "Lydopptak"), - ("File Connection", "Filtilkobling"), - ("Screen Connection", "Skjermtilkobing"), ("Do you accept?", "Akepterer du?"), ("Open System Setting", "Åpne systeminnstillinger"), ("How to get Android input permission?", "Hvordan får jeg en Android-input tillatelse?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Vis kamera"), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Vis kamera"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index b7b50cf9084..516e0db95ef 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Geen toestemming voor bestandsoverdracht"), ("Note", "Opmerking"), ("Connection", "Verbinding"), - ("Share Screen", "Scherm Delen"), + ("Share screen", "Scherm Delen"), ("Chat", "Chat"), ("Total", "Totaal"), ("items", "items"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Schermopname"), ("Input Control", "Invoercontrole"), ("Audio Capture", "Audio Opnemen"), - ("File Connection", "Bestandsverbinding"), - ("Screen Connection", "Schermverbinding"), ("Do you accept?", "Geeft u toestemming?"), ("Open System Setting", "Systeeminstelling Openen"), ("How to get Android input permission?", "Hoe krijg ik Android invoer toestemming?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Ongemarkeerd"), ("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"), ("Accessible devices", "Toegankelijke apparaten"), - ("View camera", "Camera bekijken"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgrade de RustDesk client naar versie {} of nieuwer op de externe computer!"), ("view_camera_unsupported_tip", "Het externe apparaat ondersteunt geen cameraweergave."), - ("Enable camera", "Camera inschakelen"), - ("No cameras", "Geen camera's"), ("d3d_render_tip", "Wanneer D3D-rendering is ingeschakeld kan het externe scherm op sommige apparaten, zwart zijn."), ("Use D3D rendering", "Gebruik D3D-rendering"), ("Printer", "Printer"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "Eenmalig numeriek wachtwoord"), ("Enable IPv6 P2P connection", "IPv6 P2P-verbinding inschakelen"), ("Enable UDP hole punching", "UDP-hole punching inschakelen"), + ("View camera", "Camera bekijken"), + ("Enable camera", "Camera inschakelen"), + ("No cameras", "Geen camera's"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 5b79b679653..3086e9dd04c 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Brak uprawnień na przesyłanie plików"), ("Note", "Notatka"), ("Connection", "Połączenie"), - ("Share Screen", "Udostępnij ekran"), + ("Share screen", "Udostępnij ekran"), ("Chat", "Czat"), ("Total", "Łącznie"), ("items", "elementów"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Przechwytywanie ekranu"), ("Input Control", "Kontrola wejścia"), ("Audio Capture", "Przechwytywanie dźwięku"), - ("File Connection", "Przekazywanie plików"), - ("Screen Connection", "Przekazywanie ekranu"), ("Do you accept?", "Akceptujesz?"), ("Open System Setting", "Otwórz ustawienia systemowe"), ("How to get Android input permission?", "Jak uzyskać uprawnienia do wprowadzania danych w systemie Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Bez etykiety"), ("new-version-of-{}-tip", "Dostępna jest nowa wersja {}"), ("Accessible devices", "Dostępne urządzenia"), - ("View camera", "Podgląd kamery"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Proszę zaktualizować zdalny klient RustDesk do wersji {} lub nowszej!"), ("view_camera_unsupported_tip", "Zdalne urządzenie nie obsługuje podglądu kamery."), - ("Enable camera", "Włącz kamerę"), - ("No cameras", "Brak kamer"), ("d3d_render_tip", "Kiedy włączenie renderowania D3D jest włączone, ekran zdalnej kontroli może być czarny w niektórych przypadkach"), ("Use D3D rendering", "Użyj renderowania D3D"), ("Printer", "Drukarka"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "Jednorazowe hasło cyfrowe"), ("Enable IPv6 P2P connection", "Włącz połączenie P2P IPv6"), ("Enable UDP hole punching", "Włącz tworzenie tunelu UDP"), + ("View camera", "Podgląd kamery"), + ("Enable camera", "Włącz kamerę"), + ("No cameras", "Brak kamer"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 8a79231593d..c007c3114c7 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Sem permissões de transferência de ficheiro"), ("Note", "Nota"), ("Connection", "Ligação"), - ("Share Screen", "Partilhar ecrã"), + ("Share screen", "Partilhar ecrã"), ("Chat", "Conversar"), ("Total", "Total"), ("items", "itens"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Captura de Ecran"), ("Input Control", "Controle de Entrada"), ("Audio Capture", "Captura de Áudio"), - ("File Connection", "Ligação de Arquivo"), - ("Screen Connection", "Ligação de Ecran"), ("Do you accept?", "Aceita?"), ("Open System Setting", "Abrir Configurações do Sistema"), ("How to get Android input permission?", "Como activar a permissão de entrada do Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Ver câmara"), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Ver câmara"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5148c0e462a..12199a9ac20 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Sem permissão para transferência de arquivo"), ("Note", "Nota"), ("Connection", "Conexão"), - ("Share Screen", "Compartilhar Tela"), + ("Share screen", "Compartilhar Tela"), ("Chat", "Chat"), ("Total", "Total"), ("items", "itens"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Captura de Tela"), ("Input Control", "Controle de Entrada"), ("Audio Capture", "Captura de Áudio"), - ("File Connection", "Conexão de Arquivo"), - ("Screen Connection", "Conexão de Tela"), ("Do you accept?", "Você aceita?"), ("Open System Setting", "Abrir Configurações do Sistema"), ("How to get Android input permission?", "Como habilitar a permissão de entrada do Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Sem etiqueta"), ("new-version-of-{}-tip", "Uma nova versão de {} está disponível"), ("Accessible devices", "Dispositivos acessíveis"), - ("View camera", "Visualizar câmera"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualize o cliente RustDesk para a versão {} ou superior no lado remoto."), ("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."), - ("Enable camera", "Ativar câmera"), - ("No cameras", "Sem câmeras"), ("d3d_render_tip", "Em algumas máquinas, a tela do controle remoto pode ficar preta ao usar a renderização D3D."), ("Use D3D rendering", "Usar renderização D3D"), ("Printer", "Impressora"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Visualizar câmera"), + ("Enable camera", "Ativar câmera"), + ("No cameras", "Sem câmeras"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 480b14af8ef..8ff39d53fc0 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nicio permisiune pentru transferul de fișiere"), ("Note", "Reține"), ("Connection", "Conexiune"), - ("Share Screen", "Partajează ecran"), + ("Share screen", "Partajează ecran"), ("Chat", "Mesaje"), ("Total", "Total"), ("items", "elemente"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Capturare ecran"), ("Input Control", "Control intrări"), ("Audio Capture", "Capturare audio"), - ("File Connection", "Conexiune fișier"), - ("Screen Connection", "Conexiune ecran"), ("Do you accept?", "Accepți?"), ("Open System Setting", "Deschide setări sistem"), ("How to get Android input permission?", "Cum autorizez dispozitive de intrare pe Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Vezi camera"), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Vezi camera"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 15e156e9208..b84161a897b 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Нет разрешения на передачу файлов"), ("Note", "Заметка"), ("Connection", "Подключение"), - ("Share Screen", "Демонстрация экрана"), + ("Share screen", "Демонстрация экрана"), ("Chat", "Чат"), ("Total", "Всего"), ("items", "элементы"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Захват экрана"), ("Input Control", "Управление вводом"), ("Audio Capture", "Захват аудио"), - ("File Connection", "Подключение передачи файлов"), - ("Screen Connection", "Подключение просмотра/управления экраном"), ("Do you accept?", "Вы согласны?"), ("Open System Setting", "Открыть настройки системы"), ("How to get Android input permission?", "Как получить разрешение на ввод Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Без метки"), ("new-version-of-{}-tip", "Доступна новая версия {}"), ("Accessible devices", "Доступные устройства"), - ("View camera", "Просмотр камеры"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Обновите клиент RustDesk до версии {} или новее на удалённой стороне!"), ("view_camera_unsupported_tip", "Удалённое устройство не поддерживает просмотр камеры."), - ("Enable camera", "Включить камеру"), - ("No cameras", "Камера отсутствует"), ("d3d_render_tip", "При включении визуализации D3D на некоторых устройствах удалённый экран может быть чёрным."), ("Use D3D rendering", "Использовать визуализацию D3D"), ("Printer", "Принтер"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "Цифровой одноразовый пароль"), ("Enable IPv6 P2P connection", "Использовать подключение IPv6 P2P"), ("Enable UDP hole punching", "Использовать UDP hole punching"), + ("View camera", "Просмотр камеры"), + ("Enable camera", "Включить камеру"), + ("No cameras", "Камера отсутствует"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 3fc686b1537..9610ca83cc3 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Perunu permissu pro sa tràmuda de documentos"), ("Note", "Nota"), ("Connection", "Connessione"), - ("Share Screen", "Cumpartzi ischermu"), + ("Share screen", "Cumpartzi ischermu"), ("Chat", "Tzarrada"), ("Total", "Totale"), ("items", "Elementos"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Catura de ischermu"), ("Input Control", "Controllu atziones"), ("Audio Capture", "Catura de s'àudio"), - ("File Connection", "Connessione documentos"), - ("Screen Connection", "Connessione ischermu"), ("Do you accept?", "Atzetas?"), ("Open System Setting", "Aberi sas impostatziones de sistema"), ("How to get Android input permission?", "Comente otènnere s'autorizatzione de intrada (input) in Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Chene tag"), ("new-version-of-{}-tip", "B'at una versione noa de {} a disponimentu"), ("Accessible devices", "Dispositivos atzessìbiles"), - ("View camera", "Mustra sa càmera"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualiza su cliente RustDesk remotu a sa versione {} o prus noa!"), ("view_camera_unsupported_tip", "Su dispositivu remotu non suportat sa visualizatzione de sa càmera"), - ("Enable camera", "Abìlita sa càmera"), - ("No cameras", "Peruna càmera"), ("d3d_render_tip", "Cando sa renderizatzione D3D est abilitada, s'ischermu de controllu remotu diat pòdere èssere nieddu in unas cantas màchinas"), ("Use D3D rendering", "Imprea sa renderizatzione D3D"), ("Printer", "Imprentadora"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Mustra sa càmera"), + ("Enable camera", "Abìlita sa càmera"), + ("No cameras", "Peruna càmera"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 29c92bf5fef..6f8968ce0f3 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Prenos súborov nie je povolený"), ("Note", "Poznámka"), ("Connection", "Pripojenie"), - ("Share Screen", "Zdielať obrazovku"), + ("Share screen", "Zdielať obrazovku"), ("Chat", "Chat"), ("Total", "Celkom"), ("items", "položiek"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Snímanie obrazovky"), ("Input Control", "Ovládanie vstupných zariadení"), ("Audio Capture", "Snímanie zvuku"), - ("File Connection", "Pripojenie súborov"), - ("Screen Connection", "Pripojenie obrazu"), ("Do you accept?", "Súhlasíte?"), ("Open System Setting", "Otvorenie nastavení systému"), ("How to get Android input permission?", "Ako v systéme Android povoliť oprávnenie písať zo vstupného zariadenia?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Zobraziť kameru"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Aktualizujte klienta RustDesk na verziu {} alebo novšiu na vzdialenej strane!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Zobraziť kameru"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index e4ee5262307..716c59a0fa3 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Ni pravic za prenos datotek"), ("Note", "Opomba"), ("Connection", "Povezava"), - ("Share Screen", "Deli zaslon"), + ("Share screen", "Deli zaslon"), ("Chat", "Pogovor"), ("Total", "Skupaj"), ("items", "elementi"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Zajem zaslona"), ("Input Control", "Nadzor vnosa"), ("Audio Capture", "Zajem zvoka"), - ("File Connection", "Datotečna povezava"), - ("Screen Connection", "Zaslonska povezava"), ("Do you accept?", "Ali sprejmete?"), ("Open System Setting", "Odpri sistemske nastavitve"), ("How to get Android input permission?", "Kako pridobiti dovoljenje za vnos na Androidu?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Neoznačeno"), ("new-version-of-{}-tip", "Na voljo je nova različica {}"), ("Accessible devices", ""), - ("View camera", "Pogled kamere"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Prosimo, nadgradite RustDesk odjemalec na različico {} ali novejšo na oddaljeni strani."), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Pogled kamere"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index d42be522fd6..a3107e86753 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nuk ka leje për transferimin e dosjesve"), ("Note", "Shënime"), ("Connection", "Lidhja"), - ("Share Screen", "Ndaj ekranin"), + ("Share screen", "Ndaj ekranin"), ("Chat", "Biseda"), ("Total", "Total"), ("items", "artikuj"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Kapja e ekranit"), ("Input Control", "Kontrollo inputin"), ("Audio Capture", "Kapja e zërit"), - ("File Connection", "Lidhja e skedarëve"), - ("Screen Connection", "Lidhja e ekranit"), ("Do you accept?", "E pranoni"), ("Open System Setting", "Hapni cilësimet e sistemit"), ("How to get Android input permission?", "Si të merrni leje e inputit të Android"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", ""), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 37418449436..f39dd00b094 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nemate pravo prenosa datoteka"), ("Note", "Primedba"), ("Connection", "Konekcija"), - ("Share Screen", "Podeli ekran"), + ("Share screen", "Podeli ekran"), ("Chat", "Dopisivanje"), ("Total", "Ukupno"), ("items", "stavki"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Snimanje ekrana"), ("Input Control", "Kontrola unosa"), ("Audio Capture", "Snimanje zvuka"), - ("File Connection", "Spajanje preko datoteke"), - ("Screen Connection", "Podeli konekciju"), ("Do you accept?", "Prihvatate?"), ("Open System Setting", "Postavke otvorenog sistema"), ("How to get Android input permission?", "Kako dobiti pristup za Android unos?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Pregled kamere"), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Pregled kamere"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index c5a0952dab9..e2e739c1f30 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Rättigheter saknas"), ("Note", "Notering"), ("Connection", "Anslutning"), - ("Share Screen", "Dela skärm"), + ("Share screen", "Dela skärm"), ("Chat", "Chatt"), ("Total", "Totalt"), ("items", "föremål"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Skärminspelning"), ("Input Control", "Inputkontroll"), ("Audio Capture", "Ljudinspelning"), - ("File Connection", "Fil anslutning"), - ("Screen Connection", "Skärm anslutning"), ("Do you accept?", "Accepterar du?"), ("Open System Setting", "Öppna systeminställnig"), ("How to get Android input permission?", "Hur får man Android rättigheter?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Visa kamera"), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Visa kamera"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index f9ac6a3af9b..ece9ecf85fd 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", ""), ("Note", ""), ("Connection", ""), - ("Share Screen", ""), + ("Share screen", ""), ("Chat", ""), ("Total", ""), ("items", ""), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", ""), ("Input Control", ""), ("Audio Capture", ""), - ("File Connection", ""), - ("Screen Connection", ""), ("Do you accept?", ""), ("Open System Setting", ""), ("How to get Android input permission?", ""), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", ""), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 72a2d0e69c8..6fdd8af21e4 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", ""), ("Note", ""), ("Connection", ""), - ("Share Screen", ""), + ("Share screen", ""), ("Chat", ""), ("Total", ""), ("items", ""), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", ""), ("Input Control", ""), ("Audio Capture", ""), - ("File Connection", ""), - ("Screen Connection", ""), ("Do you accept?", ""), ("Open System Setting", ""), ("How to get Android input permission?", ""), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", ""), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 1b594ec10ea..1317e7998dd 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "ไม่มีสิทธิ์ในการถ่ายโอนไฟล์"), ("Note", "บันทึกข้อความ"), ("Connection", "การเชื่อมต่อ"), - ("Share Screen", "แชร์หน้าจอ"), + ("Share screen", "แชร์หน้าจอ"), ("Chat", "แชท"), ("Total", "รวม"), ("items", "รายการ"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "บันทึกหน้าจอ"), ("Input Control", "ควบคุมอินพุท"), ("Audio Capture", "บันทึกเสียง"), - ("File Connection", "การเชื่อมต่อไฟล์"), - ("Screen Connection", "การเชื่อมต่อหน้าจอ"), ("Do you accept?", "ยอมรับหรือไม่?"), ("Open System Setting", "เปิดการตั้งค่าระบบ"), ("How to get Android input permission?", "เปิดสิทธิ์การใช้งานอินพุทของแอนดรอยด์ได้อย่างไร?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "ดูกล้อง"), ("upgrade_remote_rustdesk_client_to_{}_tip", "กรุณาอัปเดต RustDesk ไคลเอนต์ไปยังเวอร์ชัน {} หรือใหม่กว่าที่ฝั่งปลายทาง!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "ดูกล้อง"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 05ff7411549..33697efeb6c 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Dosya aktarımı izni yok"), ("Note", "Not"), ("Connection", "Bağlantı"), - ("Share Screen", "Ekranı Paylaş"), + ("Share screen", "Ekranı Paylaş"), ("Chat", "Mesajlaş"), ("Total", "Toplam"), ("items", "öğeler"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Ekran görüntüsü"), ("Input Control", "Giriş Kontrolü"), ("Audio Capture", "Ses Yakalama"), - ("File Connection", "Dosya Bağlantısı"), - ("Screen Connection", "Ekran Bağlantısı"), ("Do you accept?", "Kabul ediyor musun?"), ("Open System Setting", "Sistem Ayarını Aç"), ("How to get Android input permission?", "Android giriş izni nasıl alınır?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Kamerayı görüntüle"), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Kamerayı görüntüle"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 99678520646..adc55deb760 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "沒有檔案傳輸權限"), ("Note", "備註"), ("Connection", "連線"), - ("Share Screen", "螢幕分享"), + ("Share screen", "螢幕分享"), ("Chat", "聊天"), ("Total", "總計"), ("items", "個項目"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "畫面錄製"), ("Input Control", "輸入控制"), ("Audio Capture", "音訊錄製"), - ("File Connection", "檔案連線"), - ("Screen Connection", "畫面連線"), ("Do you accept?", "是否接受?"), ("Open System Setting", "開啟系統設定"), ("How to get Android input permission?", "如何取得 Android 的輸入權限?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "無標籤"), ("new-version-of-{}-tip", "有新版本的 {} 可用"), ("Accessible devices", "可存取的裝置"), - ("View camera", "檢視相機"), ("upgrade_remote_rustdesk_client_to_{}_tip", "請將遠端 RustDesk 客戶端升級到 {} 或更新版本!"), ("view_camera_unsupported_tip", "您的遠端設備不支援查看鏡頭"), - ("Enable camera", "允許查看鏡頭"), - ("No cameras", "沒有鏡頭"), ("d3d_render_tip", "當啟用 D3D 渲染時,某些機器可能會無法顯示遠端畫面。"), ("Use D3D rendering", "使用 D3D 渲染"), ("Printer", "印表機"), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", "數字一次性密碼"), ("Enable IPv6 P2P connection", "啟用 IPv6 P2P 連線"), ("Enable UDP hole punching", "啟用 UDP 打洞"), + ("View camera", "檢視相機"), + ("Enable camera", "允許查看鏡頭"), + ("No cameras", "沒有鏡頭"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 6dcc90b7d20..c6df72e3e64 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Немає дозволу на передачу файлів"), ("Note", "Примітка"), ("Connection", "Підключення"), - ("Share Screen", "Поділитися екраном"), + ("Share screen", "Поділитися екраном"), ("Chat", "Чат"), ("Total", "Всього"), ("items", "елементи"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Захоплення екрана"), ("Input Control", "Керування введенням"), ("Audio Capture", "Захоплення аудіо"), - ("File Connection", "Файлове підключення"), - ("Screen Connection", "Підключення екрана"), ("Do you accept?", "Ви згодні?"), ("Open System Setting", "Відкрити налаштування системи"), ("How to get Android input permission?", "Як отримати дозвіл на введення в Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Без міток"), ("new-version-of-{}-tip", "Доступна нова версія {}"), ("Accessible devices", ""), - ("View camera", "Перегляд камери"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Будь ласка, оновіть RustDesk клієнт на віддаленому пристрої до версії {} чи новіше!"), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Перегляд камери"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 09ab7fc17c6..f820766e0c1 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Không có quyền truyền tệp tin"), ("Note", "Ghi nhớ"), ("Connection", "Kết nối"), - ("Share Screen", "Chia sẻ màn hình"), + ("Share screen", "Chia sẻ màn hình"), ("Chat", "Chat"), ("Total", "Tổng"), ("items", "items"), @@ -275,8 +275,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Ghi màn hình"), ("Input Control", "Điều khiển đầu vào"), ("Audio Capture", "Ghi âm thanh"), - ("File Connection", "Kết nối tệp tin"), - ("Screen Connection", "Kết nối màn hình"), ("Do you accept?", "Bạn có chấp nhận không?"), ("Open System Setting", "Mở cài đặt hệ thống"), ("How to get Android input permission?", "Cách để có quyền nhập trên Android?"), @@ -657,11 +655,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", ""), ("new-version-of-{}-tip", ""), ("Accessible devices", ""), - ("View camera", "Xem camera"), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), ("view_camera_unsupported_tip", ""), - ("Enable camera", ""), - ("No cameras", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -701,5 +696,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Numeric one-time password", ""), ("Enable IPv6 P2P connection", ""), ("Enable UDP hole punching", ""), + ("View camera", "Xem camera"), + ("Enable camera", ""), + ("No cameras", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index 0711416fd8f..433bb5f36de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,4 +72,4 @@ pub mod privacy_mode; #[cfg(windows)] pub mod virtual_display_manager; -mod kcp_stream; \ No newline at end of file +mod kcp_stream; diff --git a/src/server.rs b/src/server.rs index 4d70fa5fa92..0dcaf7e414f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -33,6 +33,8 @@ use video_service::VideoSource; use crate::ipc::Data; pub mod audio_service; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub mod terminal_service; cfg_if::cfg_if! { if #[cfg(not(target_os = "ios"))] { mod clipboard_service; @@ -146,6 +148,7 @@ pub fn new() -> ServerPtr { } } } + // Terminal service is created per connection, not globally Arc::new(RwLock::new(server)) } diff --git a/src/server/connection.rs b/src/server/connection.rs index bdf027f935d..12a3061de0a 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -169,6 +169,7 @@ pub enum AuthConnType { FileTransfer, PortForward, ViewCamera, + Terminal, } pub struct Connection { @@ -182,6 +183,7 @@ pub struct Connection { file_timer: crate::RustDeskInterval, file_transfer: Option<(String, bool)>, view_camera: bool, + terminal: bool, port_forward_socket: Option>, port_forward_address: String, tx_to_cm: mpsc::UnboundedSender, @@ -250,6 +252,9 @@ pub struct Connection { // For post requests that need to be sent sequentially. // eg. post_conn_audit tx_post_seq: mpsc::UnboundedSender<(String, Value)>, + terminal_service_id: String, + terminal_persistent: bool, + terminal_generic_service: Option>, } impl ConnInner { @@ -347,6 +352,7 @@ impl Connection { file_timer: crate::rustdesk_interval(time::interval(SEC30)), file_transfer: None, view_camera: false, + terminal: false, port_forward_socket: None, port_forward_address: "".to_owned(), tx_to_cm, @@ -410,6 +416,9 @@ impl Connection { tx_from_authed, printer_data: Vec::new(), tx_post_seq, + terminal_service_id: "".to_owned(), + terminal_persistent: false, + terminal_generic_service: None, }; let addr = hbb_common::try_into_v4(addr); if !conn.on_open(addr).await { @@ -450,7 +459,7 @@ impl Connection { let mut last_recv_time = Instant::now(); conn.stream.set_send_timeout( - if conn.file_transfer.is_some() || conn.port_forward_socket.is_some() { + if conn.file_transfer.is_some() || conn.port_forward_socket.is_some() || conn.terminal { SEND_TIMEOUT_OTHER } else { SEND_TIMEOUT_VIDEO @@ -1253,6 +1262,8 @@ impl Connection { (2, AuthConnType::PortForward) } else if self.view_camera { (3, AuthConnType::ViewCamera) + } else if self.terminal { + (4, AuthConnType::Terminal) } else { (0, AuthConnType::Remote) }; @@ -1361,8 +1372,7 @@ impl Connection { return; } #[cfg(target_os = "linux")] - if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() && !self.view_camera - { + if self.is_remote() { let mut msg = "".to_string(); if crate::platform::linux::is_login_screen_wayland() { msg = crate::client::LOGIN_SCREEN_WAYLAND.to_owned() @@ -1393,7 +1403,8 @@ impl Connection { } #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.file_transfer.is_some() { - if crate::platform::is_prelogin() { // }|| self.tx_to_cm.send(ipc::Data::Test).is_err() { + if crate::platform::is_prelogin() { + // }|| self.tx_to_cm.send(ipc::Data::Test).is_err() { username = "".to_owned(); } } @@ -1408,6 +1419,8 @@ impl Connection { pi.sas_enabled = sas_enabled; pi.features = Some(Features { privacy_mode: privacy_mode::is_privacy_mode_supported(), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + terminal: true, // Terminal feature is supported on desktop only ..Default::default() }) .into(); @@ -1417,7 +1430,7 @@ impl Connection { let mut wait_session_id_confirm = false; #[cfg(windows)] self.handle_windows_specific_session(&mut pi, &mut wait_session_id_confirm); - if self.file_transfer.is_some() { + if self.file_transfer.is_some() || self.terminal { res.set_peer_info(pi); } else if self.view_camera { let supported_encoding = scrap::codec::Encoder::supported_encoding(); @@ -1509,6 +1522,10 @@ impl Connection { } else { self.delayed_read_dir = Some((dir.to_owned(), show_hidden)); } + } else if self.terminal { + self.keyboard = false; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.init_terminal_service().await; } else if self.view_camera { if !wait_session_id_confirm { self.try_sub_camera_displays(); @@ -1531,9 +1548,16 @@ impl Connection { } } + #[inline] + fn is_remote(&self) -> bool { + self.file_transfer.is_none() + && self.port_forward_socket.is_none() + && !self.view_camera + && !self.terminal + } + fn try_sub_monitor_services(&mut self) { - let is_remote = - self.file_transfer.is_none() && self.port_forward_socket.is_none() && !self.view_camera; + let is_remote = self.is_remote(); if is_remote && !self.services_subed { self.services_subed = true; if let Some(s) = self.server.upgrade() { @@ -1651,6 +1675,7 @@ impl Connection { id: self.inner.id(), is_file_transfer: self.file_transfer.is_some(), is_view_camera: self.view_camera, + is_terminal: self.terminal, port_forward: self.port_forward_address.clone(), peer_id, name, @@ -1902,6 +1927,19 @@ impl Connection { } self.view_camera = true; } + Some(login_request::Union::Terminal(terminal)) => { + if !Connection::permission(keys::OPTION_ENABLE_TERMINAL) { + self.send_login_error("No permission of terminal").await; + sleep(1.).await; + return false; + } + self.terminal = true; + if let Some(o) = self.options_in_login.as_ref() { + self.terminal_persistent = + o.terminal_persistent.enum_value() == Ok(BoolOption::Yes); + } + self.terminal_service_id = terminal.service_id; + } Some(login_request::Union::PortForward(mut pf)) => { if !Connection::permission("enable-tunnel") { self.send_login_error("No permission of IP tunneling").await; @@ -2791,7 +2829,7 @@ impl Connection { } } else if self.view_camera { self.try_sub_camera_displays(); - } else { + } else if !self.terminal { self.try_sub_monitor_services(); } } @@ -2843,6 +2881,12 @@ impl Connection { self.refresh_video_display(Some(request.display as usize)); } } + Some(message::Union::TerminalAction(action)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + allow_err!(self.handle_terminal_action(action).await); + #[cfg(any(target_os = "android", target_os = "ios"))] + log::warn!("Terminal action received but not supported on this platform"); + } _ => {} } } @@ -3371,6 +3415,12 @@ impl Connection { } } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Ok(q) = o.terminal_persistent.enum_value() { + if q != BoolOption::NotSet { + self.update_terminal_persistence(q == BoolOption::Yes).await; + } + } } async fn turn_on_privacy(&mut self, impl_key: String) { @@ -3562,12 +3612,7 @@ impl Connection { #[cfg(windows)] fn portable_check(&mut self) { - if self.portable.is_installed - || self.file_transfer.is_some() - || self.view_camera - || self.port_forward_socket.is_some() - || !self.keyboard - { + if self.portable.is_installed || !self.is_remote() || !self.keyboard { return; } let running = portable_client::running(); @@ -3779,6 +3824,55 @@ impl Connection { msg_out.set_message_box(res); self.send(msg_out).await; } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn update_terminal_persistence(&mut self, persistent: bool) { + self.terminal_persistent = persistent; + terminal_service::set_persistent(&self.terminal_service_id, persistent).ok(); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn init_terminal_service(&mut self) { + if self.terminal_service_id.is_empty() { + self.terminal_service_id = terminal_service::generate_service_id(); + } + let s = Box::new(terminal_service::new( + self.terminal_service_id.clone(), + self.terminal_persistent, + )); + s.on_subscribe(self.inner.clone()); + self.terminal_generic_service = Some(s); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn handle_terminal_action(&mut self, action: TerminalAction) -> ResultType<()> { + let mut proxy = terminal_service::TerminalServiceProxy::new( + self.terminal_service_id.clone(), + Some(self.terminal_persistent), + ); + + match proxy.handle_action(&action) { + Ok(Some(response)) => { + let mut msg_out = Message::new(); + msg_out.set_terminal_response(response); + self.send(msg_out).await; + } + Ok(None) => { + // No response needed + } + Err(err) => { + let mut response = TerminalResponse::new(); + let mut error = TerminalError::new(); + error.message = format!("Failed to handle action: {}", err); + response.set_error(error); + let mut msg_out = Message::new(); + msg_out.set_terminal_response(response); + self.send(msg_out).await; + } + } + + Ok(()) + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { @@ -4151,6 +4245,10 @@ impl Drop for Connection { fn drop(&mut self) { #[cfg(not(any(target_os = "android", target_os = "ios")))] self.release_pressed_modifiers(); + + if let Some(s) = self.terminal_generic_service.as_ref() { + s.join(); + } } } diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs new file mode 100644 index 00000000000..60accd720f8 --- /dev/null +++ b/src/server/terminal_service.rs @@ -0,0 +1,968 @@ +use super::*; +use hbb_common::{ + anyhow::{anyhow, Context, Result}, + compress, +}; +use portable_pty::{Child, CommandBuilder, PtySize}; +use std::{ + collections::{HashMap, VecDeque}, + io::{Read, Write}, + sync::{ + mpsc::{self, Receiver, SyncSender}, + Arc, Mutex, + }, + thread, + time::{Duration, Instant}, +}; + +const MAX_OUTPUT_BUFFER_SIZE: usize = 1024 * 1024; // 1MB per terminal +const MAX_BUFFER_LINES: usize = 10000; +const MAX_SERVICES: usize = 100; // Maximum number of persistent terminal services +const SERVICE_IDLE_TIMEOUT: Duration = Duration::from_secs(3600); // 1 hour idle timeout +const CHANNEL_BUFFER_SIZE: usize = 100; // Number of messages to buffer in channel +const COMPRESS_THRESHOLD: usize = 512; // Compress terminal data larger than this + +lazy_static::lazy_static! { + // Global registry of persistent terminal services indexed by service_id + static ref TERMINAL_SERVICES: Arc>>>> = + Arc::new(Mutex::new(HashMap::new())); + + // Cleanup task handle + static ref CLEANUP_TASK: Arc>>> = Arc::new(Mutex::new(None)); + + // List of terminal child processes to check for zombies + static ref TERMINAL_TASKS: Arc>>> = Arc::new(Mutex::new(Vec::new())); +} + +/// Service metadata that is sent to clients +#[derive(Clone, Debug)] +pub struct ServiceMetadata { + pub service_id: String, + pub created_at: Instant, + pub terminal_count: usize, + pub is_persistent: bool, +} + +/// Generate a new persistent service ID +pub fn generate_service_id() -> String { + format!("ts_{}", uuid::Uuid::new_v4()) +} + +fn get_default_shell() -> String { + #[cfg(target_os = "windows")] + { + // Try PowerShell Core first (cross-platform version) + // Common installation paths for PowerShell Core + let pwsh_paths = [ + "pwsh.exe", + r"C:\Program Files\PowerShell\7\pwsh.exe", + r"C:\Program Files\PowerShell\6\pwsh.exe", + ]; + + for path in &pwsh_paths { + if std::path::Path::new(path).exists() { + return path.to_string(); + } + } + + // Try Windows PowerShell (should be available on all Windows systems) + let powershell_path = r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"; + if std::path::Path::new(powershell_path).exists() { + return powershell_path.to_string(); + } + + // Final fallback to cmd.exe + std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string()) + } + #[cfg(not(target_os = "windows"))] + { + // First try the SHELL environment variable + if let Ok(shell) = std::env::var("SHELL") { + if !shell.is_empty() { + return shell; + } + } + + // Check for common shells in order of preference + let shells = ["/bin/bash", "/bin/zsh", "/bin/sh"]; + for shell in &shells { + if std::path::Path::new(shell).exists() { + return shell.to_string(); + } + } + + // Final fallback to /bin/sh which should exist on all POSIX systems + "/bin/sh".to_string() + } +} + +/// Get or create a persistent terminal service +fn get_or_create_service( + service_id: String, + is_persistent: bool, +) -> Result>> { + let mut services = TERMINAL_SERVICES.lock().unwrap(); + + // Check service limit + if !services.contains_key(&service_id) && services.len() >= MAX_SERVICES { + return Err(anyhow!( + "Maximum number of terminal services ({}) reached", + MAX_SERVICES + )); + } + + let service = services + .entry(service_id.clone()) + .or_insert_with(|| { + log::info!( + "Creating new terminal service: {} (persistent: {})", + service_id, + is_persistent + ); + Arc::new(Mutex::new(PersistentTerminalService::new( + service_id.clone(), + is_persistent, + ))) + }) + .clone(); + + // Ensure cleanup task is running + ensure_cleanup_task(); + + Ok(service) +} + +/// Remove a service from the global registry +fn remove_service(service_id: &str) { + let mut services = TERMINAL_SERVICES.lock().unwrap(); + if let Some(service) = services.remove(service_id) { + log::info!("Removed service: {}", service_id); + // Close all terminals in the service + let sessions = service.lock().unwrap().sessions.clone(); + for (_, session) in sessions.iter() { + let mut session = session.lock().unwrap(); + if let Some(mut child) = session.child.take() { + // Kill the process + let _ = child.kill(); + add_to_reaper(child); + } + } + } +} + +/// List all active terminal services +pub fn list_services() -> Vec { + let services = TERMINAL_SERVICES.lock().unwrap(); + services + .iter() + .filter_map(|(id, service)| { + service.lock().ok().map(|svc| ServiceMetadata { + service_id: id.clone(), + created_at: svc.created_at, + terminal_count: svc.sessions.len(), + is_persistent: svc.is_persistent, + }) + }) + .collect() +} + +/// Get service by ID +pub fn get_service(service_id: &str) -> Option>> { + let services = TERMINAL_SERVICES.lock().unwrap(); + services.get(service_id).cloned() +} + +/// Clean up inactive services +pub fn cleanup_inactive_services() { + let services = TERMINAL_SERVICES.lock().unwrap(); + let now = Instant::now(); + let mut to_remove = Vec::new(); + + for (service_id, service) in services.iter() { + if let Ok(svc) = service.lock() { + // Remove non-persistent services after idle timeout + if !svc.is_persistent && now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT { + to_remove.push(service_id.clone()); + log::info!("Cleaning up idle non-persistent service: {}", service_id); + } + // Remove persistent services with no active terminals after longer timeout + else if svc.is_persistent + && svc.sessions.is_empty() + && now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT * 2 + { + to_remove.push(service_id.clone()); + log::info!("Cleaning up empty persistent service: {}", service_id); + } + } + } + + // Remove outside of iteration to avoid deadlock + drop(services); + for id in to_remove { + remove_service(&id); + } +} + +/// Add a child process to the zombie reaper +fn add_to_reaper(child: Box) { + if let Ok(mut tasks) = TERMINAL_TASKS.lock() { + tasks.push(child); + } +} + +/// Check and reap zombie terminal processes +fn check_zombie_terminals() { + let mut tasks = match TERMINAL_TASKS.lock() { + Ok(t) => t, + Err(_) => return, + }; + + let mut i = 0; + while i < tasks.len() { + match tasks[i].try_wait() { + Ok(Some(_)) => { + // Process has exited, remove it + log::info!("Process exited: {:?}", tasks[i].process_id()); + tasks.remove(i); + } + Ok(None) => { + // Still running + i += 1; + } + Err(err) => { + // Error checking status, remove it + log::info!( + "Process exited with error: {:?}, err: {err}", + tasks[i].process_id() + ); + tasks.remove(i); + } + } + } +} + +/// Ensure the cleanup task is running +fn ensure_cleanup_task() { + let mut task_handle = CLEANUP_TASK.lock().unwrap(); + if task_handle.is_none() { + let handle = std::thread::spawn(|| { + log::info!("Started cleanup task"); + let mut last_service_cleanup = Instant::now(); + loop { + // Check for zombie processes every 100ms + check_zombie_terminals(); + + // Check for inactive services every 5 minutes + if last_service_cleanup.elapsed() > Duration::from_secs(300) { + cleanup_inactive_services(); + last_service_cleanup = Instant::now(); + } + + std::thread::sleep(Duration::from_millis(100)); + } + }); + *task_handle = Some(handle); + } +} + +pub fn new(service_id: String, is_persistent: bool) -> GenericService { + // Create the service with initial persistence setting + allow_err!(get_or_create_service(service_id.clone(), is_persistent)); + let svc = EmptyExtraFieldService::new(service_id.clone(), false); + GenericService::run(&svc.clone(), move |sp| run(sp, service_id.clone())); + svc.sp +} + +fn run(sp: EmptyExtraFieldService, service_id: String) -> ResultType<()> { + while sp.ok() { + let responses = TerminalServiceProxy::new(service_id.clone(), None).read_outputs(); + for response in responses { + let mut msg_out = Message::new(); + msg_out.set_terminal_response(response); + sp.send(msg_out); + } + + thread::sleep(Duration::from_millis(30)); // Read at ~33fps for responsive terminal + } + + // Clean up non-persistent service when loop exits + if let Some(service) = get_service(&service_id) { + let should_remove = !service.lock().unwrap().is_persistent; + if should_remove { + remove_service(&service_id); + } + } + + Ok(()) +} + +/// Output buffer for terminal session +struct OutputBuffer { + lines: VecDeque>, + total_size: usize, + last_line_incomplete: bool, +} + +impl OutputBuffer { + fn new() -> Self { + Self { + lines: VecDeque::new(), + total_size: 0, + last_line_incomplete: false, + } + } + + fn append(&mut self, data: &[u8]) { + if data.is_empty() { + return; + } + + // Handle incomplete lines + let mut start = 0; + if self.last_line_incomplete { + if let Some(last_line) = self.lines.back_mut() { + // Find first newline in new data + if let Some(newline_pos) = data.iter().position(|&b| b == b'\n') { + last_line.extend_from_slice(&data[..=newline_pos]); + start = newline_pos + 1; + self.last_line_incomplete = false; + } else { + // Still no newline, append all + last_line.extend_from_slice(data); + self.total_size += data.len(); + return; + } + } + } + + // Process remaining data + let remaining = &data[start..]; + let ends_with_newline = remaining.last() == Some(&b'\n'); + + // Split by lines + let lines: Vec<&[u8]> = remaining.split(|&b| b == b'\n').collect(); + + for (i, line) in lines.iter().enumerate() { + if i == lines.len() - 1 && !ends_with_newline && !line.is_empty() { + // Last line without newline + self.last_line_incomplete = true; + } + + if !line.is_empty() || i < lines.len() - 1 { + let mut line_data = line.to_vec(); + if i < lines.len() - 1 || ends_with_newline { + line_data.push(b'\n'); + } + + self.total_size += line_data.len(); + self.lines.push_back(line_data); + } + } + + // Trim old data if buffer is too large + while self.total_size > MAX_OUTPUT_BUFFER_SIZE || self.lines.len() > MAX_BUFFER_LINES { + if let Some(removed) = self.lines.pop_front() { + self.total_size -= removed.len(); + } + } + } + + fn get_recent(&self, max_bytes: usize) -> Vec { + let mut result = Vec::new(); + let mut size = 0; + + // Get recent lines up to max_bytes + for line in self.lines.iter().rev() { + if size + line.len() > max_bytes { + break; + } + size += line.len(); + result.splice(0..0, line.iter().cloned()); + } + + result + } +} + +pub struct TerminalSession { + pub created_at: Instant, + last_activity: Instant, + pty_pair: Option, + child: Option>, + // Channel for sending input to the writer thread + input_tx: Option>>, + // Channel for receiving output from the reader thread + output_rx: Option>>, + // Thread handles + reader_thread: Option>, + writer_thread: Option>, + output_buffer: OutputBuffer, + title: String, + pid: u32, + rows: u16, + cols: u16, + // Track if we've already sent the closed message + closed_message_sent: bool, +} + +impl TerminalSession { + fn new(terminal_id: i32, rows: u16, cols: u16) -> Self { + Self { + created_at: Instant::now(), + last_activity: Instant::now(), + pty_pair: None, + child: None, + input_tx: None, + output_rx: None, + reader_thread: None, + writer_thread: None, + output_buffer: OutputBuffer::new(), + title: format!("Terminal {}", terminal_id), + pid: 0, + rows, + cols, + closed_message_sent: false, + } + } + + fn update_activity(&mut self) { + self.last_activity = Instant::now(); + } +} + +impl Drop for TerminalSession { + fn drop(&mut self) { + // Drop the input channel to signal writer thread to exit + drop(self.input_tx.take()); + + // Wait for threads to finish + if let Some(writer_thread) = self.writer_thread.take() { + let _ = writer_thread.join(); + } + if let Some(reader_thread) = self.reader_thread.take() { + let _ = reader_thread.join(); + } + + // Ensure child process is properly handled when session is dropped + if let Some(mut child) = self.child.take() { + let _ = child.kill(); + add_to_reaper(child); + } + } +} + +/// Persistent terminal service that can survive connection drops +pub struct PersistentTerminalService { + service_id: String, + sessions: HashMap>>, + pub created_at: Instant, + last_activity: Instant, + pub is_persistent: bool, +} + +impl PersistentTerminalService { + pub fn new(service_id: String, is_persistent: bool) -> Self { + Self { + service_id, + sessions: HashMap::new(), + created_at: Instant::now(), + last_activity: Instant::now(), + is_persistent, + } + } + + fn update_activity(&mut self) { + self.last_activity = Instant::now(); + } + + /// Get list of terminal metadata + pub fn list_terminals(&self) -> Vec<(i32, String, u32, Instant)> { + self.sessions + .iter() + .map(|(id, session)| { + let s = session.lock().unwrap(); + (*id, s.title.clone(), s.pid, s.created_at) + }) + .collect() + } + + /// Get buffered output for a terminal + pub fn get_terminal_buffer(&self, terminal_id: i32, max_bytes: usize) -> Option> { + self.sessions.get(&terminal_id).map(|session| { + let session = session.lock().unwrap(); + session.output_buffer.get_recent(max_bytes) + }) + } + + /// Get terminal info for recovery + pub fn get_terminal_info(&self, terminal_id: i32) -> Option<(u16, u16, Vec)> { + self.sessions.get(&terminal_id).map(|session| { + let session = session.lock().unwrap(); + ( + session.rows, + session.cols, + session.output_buffer.get_recent(4096), + ) + }) + } + + /// Check if service has active terminals + pub fn has_active_terminals(&self) -> bool { + !self.sessions.is_empty() + } +} + +pub struct TerminalServiceProxy { + service_id: String, + is_persistent: bool, +} + +pub fn set_persistent(service_id: &str, is_persistent: bool) -> Result<()> { + if let Some(service) = get_service(service_id) { + service.lock().unwrap().is_persistent = is_persistent; + Ok(()) + } else { + Err(anyhow!("Service {} not found", service_id)) + } +} + +impl TerminalServiceProxy { + pub fn new(service_id: String, is_persistent: Option) -> Self { + // Get persistence from the service if it exists + let is_persistent = + is_persistent.unwrap_or(if let Some(service) = get_service(&service_id) { + service.lock().unwrap().is_persistent + } else { + false + }); + TerminalServiceProxy { + service_id, + is_persistent, + } + } + + pub fn get_service_id(&self) -> &str { + &self.service_id + } + + pub fn handle_action(&mut self, action: &TerminalAction) -> Result> { + let service = match get_service(&self.service_id) { + Some(s) => s, + None => { + let mut response = TerminalResponse::new(); + let mut error = TerminalError::new(); + error.message = format!("Terminal service {} not found", self.service_id); + response.set_error(error); + return Ok(Some(response)); + } + }; + service.lock().unwrap().update_activity(); + match &action.union { + Some(terminal_action::Union::Open(open)) => { + self.handle_open(&mut service.lock().unwrap(), open) + } + Some(terminal_action::Union::Resize(resize)) => { + let session = service + .lock() + .unwrap() + .sessions + .get(&resize.terminal_id) + .cloned(); + self.handle_resize(session, resize) + } + Some(terminal_action::Union::Data(data)) => { + let session = service + .lock() + .unwrap() + .sessions + .get(&data.terminal_id) + .cloned(); + self.handle_data(session, data) + } + Some(terminal_action::Union::Close(close)) => { + self.handle_close(&mut service.lock().unwrap(), close) + } + _ => Ok(None), + } + } + + fn handle_open( + &self, + service: &mut PersistentTerminalService, + open: &OpenTerminal, + ) -> Result> { + let mut response = TerminalResponse::new(); + + // Check if terminal already exists + if let Some(session_arc) = service.sessions.get(&open.terminal_id) { + // Reconnect to existing terminal + let session = session_arc.lock().unwrap(); + let mut opened = TerminalOpened::new(); + opened.terminal_id = open.terminal_id; + opened.success = true; + opened.message = "Reconnected to existing terminal".to_string(); + opened.pid = session.pid; + // Return service_id for persistent sessions + if self.is_persistent { + opened.service_id = self.service_id.clone(); + } + response.set_opened(opened); + + // Send buffered output + let buffer = session.output_buffer.get_recent(4096); + if !buffer.is_empty() { + // We'll need to send this separately or extend the protocol + // For now, just acknowledge the reconnection + } + + return Ok(Some(response)); + } + + // Create new terminal session + log::info!( + "Creating new terminal {} for service: {}", + open.terminal_id, + service.service_id + ); + let mut session = + TerminalSession::new(open.terminal_id, open.rows as u16, open.cols as u16); + + let pty_size = PtySize { + rows: open.rows as u16, + cols: open.cols as u16, + pixel_width: 0, + pixel_height: 0, + }; + + log::debug!("Opening PTY with size: {}x{}", open.rows, open.cols); + let pty_system = portable_pty::native_pty_system(); + let pty_pair = pty_system.openpty(pty_size).context("Failed to open PTY")?; + + // Use default shell for the platform + let shell = get_default_shell(); + log::debug!("Using shell: {}", shell); + let cmd = CommandBuilder::new(&shell); + + log::debug!("Spawning shell process..."); + let child = pty_pair + .slave + .spawn_command(cmd) + .context("Failed to spawn command")?; + + let writer = pty_pair + .master + .take_writer() + .context("Failed to get writer")?; + + let reader = pty_pair + .master + .try_clone_reader() + .context("Failed to get reader")?; + + session.pid = child.process_id().unwrap_or(0) as u32; + + // Create channels for input/output + let (input_tx, input_rx) = mpsc::sync_channel::>(CHANNEL_BUFFER_SIZE); + let (output_tx, output_rx) = mpsc::sync_channel::>(CHANNEL_BUFFER_SIZE); + + // Spawn writer thread + let terminal_id = open.terminal_id; + let writer_thread = thread::spawn(move || { + let mut writer = writer; + while let Ok(data) = input_rx.recv() { + if let Err(e) = writer.write_all(&data) { + log::error!("Terminal {} write error: {}", terminal_id, e); + break; + } + if let Err(e) = writer.flush() { + log::error!("Terminal {} flush error: {}", terminal_id, e); + } + } + log::debug!("Terminal {} writer thread exiting", terminal_id); + }); + + // Spawn reader thread + let terminal_id = open.terminal_id; + let reader_thread = thread::spawn(move || { + let mut reader = reader; + let mut buf = vec![0u8; 4096]; + loop { + match reader.read(&mut buf) { + Ok(0) => { + // EOF + break; + } + Ok(n) => { + let data = buf[..n].to_vec(); + // Try to send, if channel is full, drop the data + match output_tx.try_send(data) { + Ok(_) => {} + Err(mpsc::TrySendError::Full(_)) => { + log::debug!( + "Terminal {} output channel full, dropping data", + terminal_id + ); + } + Err(mpsc::TrySendError::Disconnected(_)) => { + log::debug!("Terminal {} output channel disconnected", terminal_id); + break; + } + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // For non-blocking I/O, sleep briefly + thread::sleep(Duration::from_millis(10)); + } + Err(e) => { + log::error!("Terminal {} read error: {}", terminal_id, e); + break; + } + } + } + log::debug!("Terminal {} reader thread exiting", terminal_id); + }); + + session.pty_pair = Some(pty_pair); + session.child = Some(child); + session.input_tx = Some(input_tx); + session.output_rx = Some(output_rx); + session.reader_thread = Some(reader_thread); + session.writer_thread = Some(writer_thread); + + let mut opened = TerminalOpened::new(); + opened.terminal_id = open.terminal_id; + opened.success = true; + opened.message = "Terminal opened".to_string(); + opened.pid = session.pid; + // Return service_id for persistent sessions + if self.is_persistent { + opened.service_id = service.service_id.clone(); + } + response.set_opened(opened); + + log::info!( + "Terminal {} opened successfully with PID {}", + open.terminal_id, + session.pid + ); + + // Store the session + service + .sessions + .insert(open.terminal_id, Arc::new(Mutex::new(session))); + + Ok(Some(response)) + } + + fn handle_resize( + &self, + session: Option>>, + resize: &ResizeTerminal, + ) -> Result> { + if let Some(session_arc) = session { + let mut session = session_arc.lock().unwrap(); + session.update_activity(); + session.rows = resize.rows as u16; + session.cols = resize.cols as u16; + + if let Some(pty_pair) = &session.pty_pair { + pty_pair.master.resize(PtySize { + rows: resize.rows as u16, + cols: resize.cols as u16, + pixel_width: 0, + pixel_height: 0, + })?; + } + } + Ok(None) + } + + fn handle_data( + &self, + session: Option>>, + data: &TerminalData, + ) -> Result> { + if let Some(session_arc) = session { + let mut session = session_arc.lock().unwrap(); + session.update_activity(); + if let Some(input_tx) = &session.input_tx { + // Send data to writer thread + if let Err(e) = input_tx.send(data.data.to_vec()) { + log::error!( + "Failed to send data to terminal {}: {}", + data.terminal_id, + e + ); + } + } + } + + Ok(None) + } + + fn handle_close( + &self, + service: &mut PersistentTerminalService, + close: &CloseTerminal, + ) -> Result> { + let mut response = TerminalResponse::new(); + + // Always close and remove the terminal + if let Some(session_arc) = service.sessions.remove(&close.terminal_id) { + let mut session = session_arc.lock().unwrap(); + let exit_code = if let Some(mut child) = session.child.take() { + child.kill()?; + add_to_reaper(child); + -1 // -1 indicates forced termination + } else { + 0 + }; + + let mut closed = TerminalClosed::new(); + closed.terminal_id = close.terminal_id; + closed.exit_code = exit_code; + response.set_closed(closed); + Ok(Some(response)) + } else { + Ok(None) + } + } + + pub fn read_outputs(&self) -> Vec { + let service = match get_service(&self.service_id) { + Some(s) => s, + None => { + return vec![]; + } + }; + + // Get session references with minimal service lock time + let sessions: Vec<(i32, Arc>)> = { + let service = service.lock().unwrap(); + service + .sessions + .iter() + .map(|(id, session)| (*id, session.clone())) + .collect() + }; + + let mut responses = Vec::new(); + let mut closed_terminals = Vec::new(); + + // Process each session with its own lock + for (terminal_id, session_arc) in sessions { + if let Ok(mut session) = session_arc.try_lock() { + // Check if reader thread is still alive and we haven't sent closed message yet + let mut should_send_closed = false; + if !session.closed_message_sent { + if let Some(thread) = &session.reader_thread { + if thread.is_finished() { + should_send_closed = true; + session.closed_message_sent = true; + } + } + } + + // Read from output channel + let mut has_activity = false; + let mut received_data = Vec::new(); + if let Some(output_rx) = &session.output_rx { + // Try to read all available data + while let Ok(data) = output_rx.try_recv() { + has_activity = true; + received_data.push(data); + } + } + + // Update buffer after reading + for data in &received_data { + session.output_buffer.append(data); + } + + // Process received data for responses + for data in received_data { + let mut response = TerminalResponse::new(); + let mut terminal_data = TerminalData::new(); + terminal_data.terminal_id = terminal_id; + + // Compress data if it exceeds threshold + if data.len() > COMPRESS_THRESHOLD { + let compressed = compress::compress(&data); + if compressed.len() < data.len() { + terminal_data.data = bytes::Bytes::from(compressed); + terminal_data.compressed = true; + } else { + // Compression didn't help, send uncompressed + terminal_data.data = bytes::Bytes::from(data); + } + } else { + terminal_data.data = bytes::Bytes::from(data); + } + + response.set_data(terminal_data); + responses.push(response); + } + + if has_activity { + session.update_activity(); + } + + if should_send_closed { + closed_terminals.push(terminal_id); + } + } + } + + // Clean up closed terminals (requires service lock briefly) + if !closed_terminals.is_empty() { + let mut sessions = service.lock().unwrap().sessions.clone(); + for terminal_id in closed_terminals { + let mut exit_code = 0; + + if !self.is_persistent { + if let Some(session_arc) = sessions.remove(&terminal_id) { + service.lock().unwrap().sessions.remove(&terminal_id); + let mut session = session_arc.lock().unwrap(); + // Take the child and add to zombie reaper + if let Some(mut child) = session.child.take() { + // Try to get exit code if available + if let Ok(Some(status)) = child.try_wait() { + exit_code = status.exit_code() as i32; + } + add_to_reaper(child); + } + } + } else { + // For persistent sessions, just clear the child reference + if let Some(session_arc) = sessions.get(&terminal_id) { + let mut session = session_arc.lock().unwrap(); + if let Some(mut child) = session.child.take() { + // Try to get exit code if available + if let Ok(Some(status)) = child.try_wait() { + exit_code = status.exit_code() as i32; + } + add_to_reaper(child); + } + } + } + + let mut response = TerminalResponse::new(); + let mut closed = TerminalClosed::new(); + closed.terminal_id = terminal_id; + closed.exit_code = exit_code; + response.set_closed(closed); + responses.push(response); + } + } + + responses + } + + /// Cleanup when connection drops + pub fn on_disconnect(&self) { + if !self.is_persistent { + // Remove non-persistent service + remove_service(&self.service_id); + } + } +} diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 5e56896affb..57e6b37dd16 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -20,6 +20,7 @@ impl InvokeUiCM for SciterHandler { client.id, client.is_file_transfer, client.is_view_camera, + client.is_terminal, client.port_forward.clone(), client.peer_id.clone(), client.name.clone(), diff --git a/src/ui/cm.tis b/src/ui/cm.tis index fc18bdfd30a..479e26f9283 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -29,7 +29,7 @@ class Body: Reactor.Component }; var right_style = show_chat ? "" : "display: none"; var disconnected = c.disconnected; - var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && c.port_forward.length == 0; + var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && !c.is_view_camera && !c.is_terminal && c.port_forward.length == 0; var show_accept_btn = handler.get_option('approve-mode') != 'password'; // below size:* is a workaround for Linux, it already set in css, but not work, shit sciter return
    @@ -48,8 +48,8 @@ class Body: Reactor.Component
    - {c.is_file_transfer || c.port_forward || disconnected ? "" :
    {translate('Permissions')}
    } - {c.is_file_transfer || c.port_forward || disconnected ? "" :
    + {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
    {translate('Permissions')}
    } + {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
    @@ -60,6 +60,9 @@ class Body: Reactor.Component
    } + {c.is_file_transfer ?
    {translate('Transfer file')}
    : ""} + {c.is_view_camera ?
    {translate('View camera')}
    : ""} + {c.is_terminal ?
    {translate('Terminal')}
    : ""} {c.port_forward ?
    Port Forwarding: {c.port_forward}
    : ""}
    @@ -72,10 +75,10 @@ class Body: Reactor.Component {auth && !disconnected ? : "" } {auth && disconnected ? : "" }
    - {c.is_file_transfer || c.port_forward ? "" :
    {svg_chat}
    } + {c.is_file_transfer || c.is_terminal || c.port_forward ? "" :
    {svg_chat}
    }
    - {c.is_file_transfer || c.port_forward ? "" : } + {c.is_file_transfer || c.is_terminal || c.port_forward ? "" : }
    ; } @@ -356,7 +359,7 @@ function bring_to_top(idx=-1) { } } -handler.addConnection = function(id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { +handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { stdout.println("new connection #" + id + ": " + peer_id); var conn; connections.map(function(c) { @@ -373,7 +376,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, port_forw }); if (!name) name = "NA"; conn = { - id: id, is_file_transfer: is_file_transfer, peer_id: peer_id, + id: id, is_file_transfer: is_file_transfer, is_view_camera: is_view_camera, is_terminal: is_terminal, peer_id: peer_id, port_forward: port_forward, name: name, authorized: authorized, time: new Date(), now: new Date(), keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, diff --git a/src/ui/index.tis b/src/ui/index.tis index fd505854462..bee8bba8c53 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -313,7 +313,9 @@ class MyIdMenu: Reactor.Component {
  • {svg_checkmark}{translate('Enable keyboard/mouse')}
  • {svg_checkmark}{translate('Enable clipboard')}
  • -
  • {svg_checkmark}{translate('Enable file transfer')}
  • +
  • {svg_checkmark}{translate('Enable file transfer')}
  • +
  • {svg_checkmark}{translate('Enable camera')}
  • +
  • {svg_checkmark}{translate('Enable terminal')}
  • {svg_checkmark}{translate('Enable remote restart')}
  • {svg_checkmark}{translate('Enable TCP tunneling')}
  • {is_win ?
  • {svg_checkmark}{translate('Enable blocking user input')}
  • : ""} diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 82cac19b18a..f99e2de6ee1 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -313,16 +313,10 @@ impl InvokeUiSession for SciterHandler { fn on_connected(&self, conn_type: ConnType) { match conn_type { - ConnType::RDP => {} - ConnType::PORT_FORWARD => {} - ConnType::FILE_TRANSFER => {} - ConnType::VIEW_CAMERA => {} ConnType::DEFAULT_CONN => { crate::keyboard::client::start_grab_loop(); } - // Left empty code from compilation. - // Please replace the code in the PR. - ConnType::VIEW_CAMERA => {} + _ => {} } } @@ -387,6 +381,11 @@ impl InvokeUiSession for SciterHandler { fn handle_screenshot_resp(&self, _sid: String, msg: String) { self.call("screenshot", &make_args!(msg)); } + + fn handle_terminal_response(&self, _response: TerminalResponse) { + // Terminal support is not implemented for Sciter UI + // This is a stub implementation to satisfy the trait requirements + } } pub struct SciterSession(Session); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 1da297a4ef8..880f0ca61df 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -48,6 +48,7 @@ pub struct Client { pub disconnected: bool, pub is_file_transfer: bool, pub is_view_camera: bool, + pub is_terminal: bool, pub port_forward: String, pub name: String, pub peer_id: String, @@ -130,6 +131,7 @@ impl ConnectionManager { id: i32, is_file_transfer: bool, is_view_camera: bool, + is_terminal: bool, port_forward: String, peer_id: String, name: String, @@ -150,6 +152,7 @@ impl ConnectionManager { disconnected: false, is_file_transfer, is_view_camera, + is_terminal, port_forward, name: name.clone(), peer_id: peer_id.clone(), @@ -206,7 +209,7 @@ impl ConnectionManager { .read() .unwrap() .iter() - .filter(|(_k, v)| !v.is_file_transfer) + .filter(|(_k, v)| !v.is_file_transfer && !v.is_terminal) .next() .is_none() { @@ -405,9 +408,9 @@ impl IpcTaskRunner { } Ok(Some(data)) => { match data { - Data::Login{id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { + Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { log::debug!("conn_id: {}", id); - self.cm.add_connection(id, is_file_transfer, is_view_camera, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); + self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); self.conn_id = id; #[cfg(target_os = "windows")] { @@ -676,6 +679,7 @@ pub async fn start_listen( id, is_file_transfer, is_view_camera, + is_terminal, port_forward, peer_id, name, @@ -695,6 +699,7 @@ pub async fn start_listen( id, is_file_transfer, is_view_camera, + is_terminal, port_forward, peer_id, name, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2328a05707d..93dde39094a 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -20,7 +20,7 @@ use uuid::Uuid; use hbb_common::fs; use hbb_common::{ allow_err, - config::{Config, LocalConfig, PeerConfig}, + config::{keys, Config, LocalConfig, PeerConfig}, get_version_number, log, message_proto::*, rendezvous_proto::ConnType, @@ -191,10 +191,18 @@ impl Session { .eq(&ConnType::FILE_TRANSFER) } + pub fn is_default(&self) -> bool { + self.lc.read().unwrap().conn_type.eq(&ConnType::DEFAULT_CONN) + } + pub fn is_view_camera(&self) -> bool { self.lc.read().unwrap().conn_type.eq(&ConnType::VIEW_CAMERA) } + pub fn is_terminal(&self) -> bool { + self.lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL) + } + pub fn is_port_forward(&self) -> bool { let conn_type = self.lc.read().unwrap().conn_type; conn_type == ConnType::PORT_FORWARD || conn_type == ConnType::RDP @@ -341,7 +349,7 @@ impl Session { pub fn toggle_option(&self, name: String) { let msg = self.lc.write().unwrap().toggle_option(name.clone()); #[cfg(all(target_os = "windows", not(feature = "flutter")))] - if name == hbb_common::config::keys::OPTION_ENABLE_FILE_COPY_PASTE { + if name == keys::OPTION_ENABLE_FILE_COPY_PASTE { self.send(Data::ToggleClipboardFile); } if let Some(msg) = msg { @@ -746,6 +754,57 @@ impl Session { self.send(Data::Message(msg_out)); } + // Terminal methods + pub fn open_terminal(&self, terminal_id: i32, rows: u32, cols: u32) { + let mut action = TerminalAction::new(); + action.set_open(OpenTerminal { + terminal_id, + rows, + cols, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_terminal_action(action); + self.send(Data::Message(msg_out)); + } + + pub fn send_terminal_input(&self, terminal_id: i32, data: String) { + let mut action = TerminalAction::new(); + action.set_data(TerminalData { + terminal_id, + data: bytes::Bytes::from(data.into_bytes()), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_terminal_action(action); + self.send(Data::Message(msg_out)); + } + + pub fn resize_terminal(&self, terminal_id: i32, rows: u32, cols: u32) { + let mut action = TerminalAction::new(); + action.set_resize(ResizeTerminal { + terminal_id, + rows, + cols, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_terminal_action(action); + self.send(Data::Message(msg_out)); + } + + pub fn close_terminal(&self, terminal_id: i32) { + let mut action = TerminalAction::new(); + action.set_close(CloseTerminal { + terminal_id, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_terminal_action(action); + self.send(Data::Message(msg_out)); + } + + pub fn capture_displays(&self, add: Vec, sub: Vec, set: Vec) { let mut misc = Misc::new(); misc.set_capture_displays(CaptureDisplays { @@ -1488,7 +1547,7 @@ impl Session { self.read_remote_dir(remote_dir, show_hidden); } } - } else { + } else if !self.is_terminal() { self.msgbox( "success", "Successful", @@ -1603,6 +1662,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn update_empty_dirs(&self, _res: ReadEmptyDirsResponse) {} fn printer_request(&self, id: i32, path: String); fn handle_screenshot_resp(&self, sid: String, msg: String); + fn handle_terminal_response(&self, response: TerminalResponse); } impl Deref for Session { @@ -1663,7 +1723,7 @@ impl Interface for Session { self.on_error("No active console user logged on, please connect and logon first."); return; } - } else if !self.is_port_forward() { + } else if !self.is_port_forward() && !self.is_terminal() { if pi.displays.is_empty() { self.lc.write().unwrap().handle_peer_info(&pi); self.update_privacy_mode(); @@ -1695,7 +1755,7 @@ impl Interface for Session { self.set_peer_info(&pi); if self.is_file_transfer() { self.close_success(); - } else if !self.is_port_forward() { + } else if !self.is_port_forward() && !self.is_terminal() { self.msgbox( "success", "Successful", diff --git a/terminal.md b/terminal.md new file mode 100644 index 00000000000..92a0973cb63 --- /dev/null +++ b/terminal.md @@ -0,0 +1,521 @@ +# RustDesk Terminal Service Implementation + +## Overview + +The RustDesk terminal service provides remote terminal/shell access with support for multiple concurrent terminal sessions per connection. It features persistence support, allowing terminal sessions to survive connection drops and be resumed later. + +## Architecture + +### Client-Side (Flutter) + +#### Terminal Connection Management +- **TerminalConnectionManager** (`flutter/lib/desktop/pages/terminal_connection_manager.dart`) + - Manages one FFI instance per peer (shared across all terminal tabs) + - Tracks persistence settings per peer + - Handles connection reference counting + +#### Terminal Models +- **TerminalModel** (`flutter/lib/models/terminal_model.dart`) + - One instance per terminal tab + - Handles terminal I/O and display using xterm package + - Manages terminal state (opened, size, buffer) + +#### UI Components +- **TerminalTabPage** (`flutter/lib/desktop/pages/terminal_tab_page.dart`) + - Manages multiple terminal tabs + - Right-click menu for persistence toggle + - Keyboard shortcuts (Cmd/Ctrl+Shift+T for new terminal) + +### Server-Side (Rust) + +#### Terminal Service Structure +```rust +TerminalService { + conn_id: i32, + service_id: String, // "tmp_{uuid}" or "persist_{uuid}" + persist: bool, +} + +PersistentTerminalService { + service_id: String, + sessions: HashMap, // terminal_id -> session + next_terminal_id: i32, + created_at: Instant, + last_activity: Instant, +} + +TerminalSession { + terminal_id: i32, + pty_pair: PtyPair, + child: Box, + writer: Box, + reader: Box, + output_buffer: OutputBuffer, // For reconnection + rows: u16, + cols: u16, +} +``` + +## Message Protocol + +### Client → Server Messages + +1. **Open Terminal** +```protobuf +TerminalAction { + open: OpenTerminal { + terminal_id: i32, + rows: u32, + cols: u32, + } +} +``` + +2. **Send Input** +```protobuf +TerminalAction { + data: TerminalData { + terminal_id: i32, + data: bytes, + } +} +``` + +3. **Resize Terminal** +```protobuf +TerminalAction { + resize: ResizeTerminal { + terminal_id: i32, + rows: u32, + cols: u32, + } +} +``` + +4. **Close Terminal** +```protobuf +TerminalAction { + close: CloseTerminal { + terminal_id: i32, + force: bool, + } +} +``` + +### Server → Client Messages + +1. **Terminal Opened** +```protobuf +TerminalResponse { + opened: TerminalOpened { + terminal_id: i32, + success: bool, + message: string, + pid: u32, + } +} +``` + +2. **Terminal Output** +```protobuf +TerminalResponse { + data: TerminalData { + terminal_id: i32, + data: bytes, // Base64 encoded in Flutter + } +} +``` + +3. **Terminal Closed** +```protobuf +TerminalResponse { + closed: TerminalClosed { + terminal_id: i32, + exit_code: i32, + } +} +``` + +## Persistence Design + +### Service ID Convention +- **Temporary**: `"tmp_{uuid}"` - Cleaned up after idle timeout +- **Persistent**: `"persist_{uuid}"` - Survives disconnections + +### Persistence Flow +1. User right-clicks terminal tab → "Enable terminal persistence" +2. Client stores persistence preference in `TerminalConnectionManager` +3. New terminals created with appropriate service ID prefix +4. Service ID saved for future reconnection (TODO: implement storage) + +### Cleanup Rules +- **Temporary services (`tmp_`)**: + - Removed after 1 hour idle time + - Immediately removed when service loop exits + +- **Persistent services**: + - Removed after 2 hours idle time IF empty + - Survive connection drops + - Can be reconnected using saved service ID + +### Cleanup Implementation + +#### 1. **Automatic Background Cleanup** +```rust +// Runs every 5 minutes +fn ensure_cleanup_task() { + tokio::spawn(async { + let mut interval = tokio::time::interval(Duration::from_secs(300)); + loop { + interval.tick().await; + cleanup_inactive_services(); + } + }); +} +``` + +#### 2. **Cleanup Logic** +```rust +fn cleanup_inactive_services() { + let now = Instant::now(); + + for (service_id, service) in services.iter() { + // Temporary services: clean up after 1 hour idle + if service_id.starts_with("tmp_") && + now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT { + to_remove.push(service_id); + } + // Persistent services: clean up after 2 hours IF empty + else if !service_id.starts_with("tmp_") && + svc.sessions.is_empty() && + now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT * 2 { + to_remove.push(service_id); + } + } +} +``` + +#### 3. **Service Loop Exit Cleanup** +```rust +fn run(sp: EmptyExtraFieldService, _conn_id: i32, service_id: String) { + // Service loop + while sp.ok() { + // Read and send terminal outputs... + } + + // Clean up temporary services immediately on exit + if service_id.starts_with("tmp_") { + remove_service(&service_id); + } +} +``` + +#### 4. **Session Cleanup Within Service** +When a terminal is closed: +- PTY process is terminated +- Terminal session removed from service's HashMap +- Resources (file descriptors, buffers) are freed +- Service continues running for other terminals + +#### 5. **Connection Drop Behavior** +```rust +impl Drop for Connection { + fn drop(&mut self) { + if self.terminal { + // Unsubscribe from terminal service + server.subscribe(&service_name, self.inner.clone(), false); + } + } +} +``` +- Connection unsubscribes from service +- Service loop continues if other subscribers exist +- If no subscribers remain, `sp.ok()` returns false → service loop exits + +#### 6. **Activity Tracking** +`last_activity` is updated when: +- New terminal opened +- Input sent to terminal +- Terminal resized +- Output read from terminal +- Any terminal operation occurs + +#### 7. **Two-Phase Cleanup Process** +```rust +// Collect services to remove (while holding lock) +let mut to_remove = Vec::new(); +for (id, service) in services.iter() { + if should_remove(service) { + to_remove.push(id); + } +} + +// Remove services (after releasing lock) +drop(services); +for id in to_remove { + remove_service(&id); +} +``` +This prevents deadlock when removing services. + +## Key Features + +### Multiple Terminals per Connection +- Single FFI connection shared by all terminal tabs +- Each terminal has unique ID within the service +- Independent PTY sessions per terminal + +### Output Buffering +- Last 1MB of output buffered per terminal +- Allows showing recent history on reconnection +- Ring buffer with line-based storage + +### Cross-Platform Support +- **Unix/Linux/macOS**: Uses default shell from `$SHELL` or `/bin/bash` +- **Windows**: Uses `%COMSPEC%` or `cmd.exe` +- PTY implementation via `portable_pty` crate + +### Non-Blocking I/O +- PTY readers set to non-blocking mode (Unix) +- Output polled at ~33fps for responsive display +- Prevents blocking when no data available + +## Current Limitations + +1. **Service ID Storage**: Client doesn't persist service IDs yet +2. **Reconnection UI**: No UI to recover previous sessions +3. **Authentication**: No per-service authentication for reconnection +4. **Resource Limits**: No configurable limits on terminals per service + +## Future Enhancements + +1. **Proper Reconnection Flow**: + - Store service IDs in peer config + - UI to list and recover previous sessions + - Show buffered output on reconnection + +2. **Security**: + - Authentication token for service recovery + - Encryption of buffered output + - Access control per terminal + +3. **Advanced Features**: + - Terminal sharing between users + - Session recording/playback + - File transfer via terminal + - Custom shell/command configuration + +## Code Locations + +- **Server Implementation**: `src/server/terminal_service.rs` +- **Connection Handler**: `src/server/connection.rs` (handle_terminal_action) +- **Client Interface**: `src/ui_session_interface.rs` (terminal methods) +- **Flutter FFI**: `src/flutter_ffi.rs` (session_open_terminal, etc.) +- **Flutter Models**: `flutter/lib/models/terminal_model.dart` +- **Flutter UI**: `flutter/lib/desktop/pages/terminal_*.dart` + +## Usage + +1. **Start Terminal Session**: + - Click terminal icon or use Ctrl/Cmd+Shift+T + - Terminal opens with default shell + +2. **Enable Persistence**: + - Right-click any terminal tab + - Select "Enable terminal persistence" + - All terminals for that peer become persistent + +3. **Multiple Terminals**: + - Click "+" button or Ctrl/Cmd+Shift+T + - Each terminal is independent + +4. **Reconnection** (TODO): + - Connect to same peer + - Previous terminals automatically restored + - Recent output displayed + +## Implementation Issues & TODOs + +### Critical Missing Features + +1. **Service ID Storage & Recovery** + - Need to store service_id in peer config when persistence enabled + - Pass service_id in LoginRequest for reconnection + - Handle service_id in server login flow + - Return terminal list in LoginResponse + +2. **Protocol Extensions Needed** + ```protobuf + // In LoginRequest + message Terminal { + string service_id = 1; // For reconnection + bool persistent = 2; // Request persistence + } + + // In LoginResponse + message TerminalServiceInfo { + string service_id = 1; + repeated TerminalSessionInfo sessions = 2; + } + ``` + +3. **Terminal Recovery Flow** + - Add RecoverTerminal action to restore specific terminal + - Send buffered output on reconnection + - Handle terminal size on recovery + - UI to show available terminals + +### Current Design Issues + +1. **Service Pattern Mismatch** + - Terminal service forced into broadcast service pattern + - Should be direct connection resource, not shared service + - Complex routing through service registry unnecessary + +2. **Global State Management** + - TERMINAL_SERVICES static HashMap may cause issues + - No proper service discovery mechanism + - Cleanup task is global, not per-connection + +3. **Resource Limits Missing** + - No limit on terminals per service + - No limit on buffer size per terminal + - No limit on total services + - Could lead to resource exhaustion + +4. **Security Concerns** + - No authentication for service recovery + - Service IDs are predictable (just UUID) + - No encryption of buffered terminal output + - No access control between users + +### Performance Optimizations Needed + +1. **Output Reading** + - Currently polls at 33fps regardless of activity + - Should use event-driven I/O (epoll/kqueue) + - Batch small outputs to reduce messages + +2. **Buffer Management** + - Ring buffer could be more efficient + - Consider compression for stored output + - Implement smart truncation (keep last N complete lines) + +3. **Message Overhead** + - Each output chunk creates new protobuf message + - Could batch multiple terminal outputs + - Consider streaming protocol for continuous output + +### Platform-Specific Issues + +1. **Windows** + - ConPTY support needs testing + - Non-blocking I/O handled differently + - Shell detection could be improved + +2. **Mobile (Android/iOS)** + - Terminal feature disabled by conditional compilation + - Need to evaluate mobile terminal support + - Touch keyboard integration needed + +### Testing Requirements + +1. **Unit Tests Needed** + - Terminal service lifecycle + - Cleanup logic edge cases + - Buffer management + - Message serialization + +2. **Integration Tests** + - Multi-terminal scenarios + - Reconnection flows + - Cleanup timing + - Resource limits + +3. **Stress Tests** + - Many terminals per connection + - Large output volumes + - Rapid connect/disconnect + - Long-running sessions + +### Alternative Designs to Consider + +1. **Direct Terminal Management** + ```rust + // In Connection struct + terminals: HashMap, + + // No service pattern, direct management + async fn handle_terminal_action(&mut self, action) { + match action { + Open => self.open_terminal(), + Data => self.terminal_input(), + // etc + } + } + ``` + +2. **Actor-Based Design** + - Each terminal as an actor + - Message passing for I/O + - Better isolation and error handling + +3. **Session Manager Service** + - One global terminal manager + - Connections request terminals from manager + - Cleaner separation of concerns + +### Documentation Gaps + +1. **API Documentation** + - Document all public methods + - Add examples for common operations + - Document error conditions + +2. **Configuration** + - Document all timeouts and limits + - How to configure shell/terminal + - Platform-specific settings + +3. **Troubleshooting Guide** + - Common issues and solutions + - Debug logging interpretation + - Performance tuning + +### Future Feature Ideas + +1. **Advanced Terminal Features** + - Terminal sharing (multiple users, one terminal) + - Session recording and playback + - File transfer through terminal (zmodem) + - Custom color schemes + - Font configuration + +2. **Integration Features** + - SSH key forwarding + - Environment variable injection + - Working directory synchronization + - Shell integration (prompt markers, etc) + +3. **Management Features** + - Terminal session monitoring + - Usage statistics + - Audit logging + - Rate limiting + +### Refactoring Suggestions + +1. **Separate Concerns** + - Split terminal_service.rs into multiple files + - Separate PTY management from service logic + - Extract buffer management to own module + +2. **Improve Error Handling** + - Use proper error types, not strings + - Add error recovery mechanisms + - Better error reporting to client + +3. **Configuration Management** + - Make timeouts configurable + - Add feature flags for experimental features + - Environment-based configuration \ No newline at end of file From f26d2a7b84a448813c55edd8b006e531d09b224e Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:13:41 +0800 Subject: [PATCH 380/506] feat: stylus support (#12196) Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 44 +++++++++++--------- flutter/lib/consts.dart | 7 ++++ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 36fb4533995..8eb0ecbc355 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -111,9 +111,13 @@ class _RawTouchGestureDetectorRegionState ); } + bool isNotTouchBasedDevice() { + return !kTouchBasedDeviceKinds.contains(lastDeviceKind); + } + onTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (handleTouch) { @@ -126,7 +130,7 @@ class _RawTouchGestureDetectorRegionState onTapUp(TapUpDetails d) async { final TapDownDetails? lastTapDownDetails = _lastTapDownDetails; _lastTapDownDetails = null; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (handleTouch) { @@ -142,7 +146,7 @@ class _RawTouchGestureDetectorRegionState } onTap() async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (!handleTouch) { @@ -153,7 +157,7 @@ class _RawTouchGestureDetectorRegionState onDoubleTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (handleTouch) { @@ -163,7 +167,7 @@ class _RawTouchGestureDetectorRegionState } onDoubleTap() async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) { @@ -179,7 +183,7 @@ class _RawTouchGestureDetectorRegionState onLongPressDown(LongPressDownDetails d) async { lastDeviceKind = d.kind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (handleTouch) { @@ -198,7 +202,7 @@ class _RawTouchGestureDetectorRegionState } onLongPressUp() async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (handleTouch) { @@ -208,7 +212,7 @@ class _RawTouchGestureDetectorRegionState // for mobiles onLongPress() async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (!ffi.ffiModel.isPeerMobile) { @@ -228,7 +232,7 @@ class _RawTouchGestureDetectorRegionState } onLongPressMoveUpdate(LongPressMoveUpdateDetails d) async { - if (!ffiModel.isPeerMobile || lastDeviceKind != PointerDeviceKind.touch) { + if (!ffiModel.isPeerMobile || isNotTouchBasedDevice()) { return; } if (handleTouch) { @@ -241,7 +245,7 @@ class _RawTouchGestureDetectorRegionState onDoubleFinerTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } _doubleFinerTapPosition = d.localPosition; @@ -250,7 +254,7 @@ class _RawTouchGestureDetectorRegionState onDoubleFinerTap(TapDownDetails d) async { lastDeviceKind = d.kind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } @@ -266,7 +270,7 @@ class _RawTouchGestureDetectorRegionState onHoldDragStart(DragStartDetails d) async { lastDeviceKind = d.kind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (!handleTouch) { @@ -275,7 +279,7 @@ class _RawTouchGestureDetectorRegionState } onHoldDragUpdate(DragUpdateDetails d) async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (!handleTouch) { @@ -284,7 +288,7 @@ class _RawTouchGestureDetectorRegionState } onHoldDragEnd(DragEndDetails d) async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (!handleTouch) { @@ -296,7 +300,7 @@ class _RawTouchGestureDetectorRegionState final TapDownDetails? lastTapDownDetails = _lastTapDownDetails; _lastTapDownDetails = null; lastDeviceKind = d.kind ?? lastDeviceKind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (handleTouch) { @@ -342,7 +346,7 @@ class _RawTouchGestureDetectorRegionState } onOneFingerPanUpdate(DragUpdateDetails d) async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { @@ -356,7 +360,7 @@ class _RawTouchGestureDetectorRegionState onOneFingerPanEnd(DragEndDetails d) async { _touchModePanStarted = false; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (isDesktop || isWebDesktop) { @@ -370,13 +374,13 @@ class _RawTouchGestureDetectorRegionState // scale + pan event onTwoFingerScaleStart(ScaleStartDetails d) { _lastTapDownDetails = null; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } } onTwoFingerScaleUpdate(ScaleUpdateDetails d) async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if ((isDesktop || isWebDesktop)) { @@ -401,7 +405,7 @@ class _RawTouchGestureDetectorRegionState } onTwoFingerScaleEnd(ScaleEndDetails d) async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if ((isDesktop || isWebDesktop)) { diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index d608df83aca..ef42318f030 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -1,3 +1,4 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -336,6 +337,12 @@ const kRemoteImageQualityCustom = 'custom'; const kIgnoreDpi = true; +const Set kTouchBasedDeviceKinds = { + PointerDeviceKind.touch, + PointerDeviceKind.stylus, + PointerDeviceKind.invertedStylus, +}; + // ================================ mobile ================================ // Magic numbers, maybe need to avoid it or use a better way to get them. From cf0d090c084d822e06fe26a328b68504e2a54be8 Mon Sep 17 00:00:00 2001 From: Naveenkumar <38713995+naveendev97@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:48:23 +0530 Subject: [PATCH 381/506] Update ta.rs (#12200) Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- src/lang/ta.rs | 1008 ++++++++++++++++++++++++------------------------ 1 file changed, 504 insertions(+), 504 deletions(-) diff --git a/src/lang/ta.rs b/src/lang/ta.rs index ece9ecf85fd..e372ce5bac8 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -3,8 +3,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "நிலை"), ("Your Desktop", "உங்கள் டெஸ்க்டாப்"), - ("desk_tip", "டெஸ்க்டாப் பயன்ப்புத்தகம்"), - ("Password", "கடவுச்சொல்"), + ("desk_tip", "டெஸ்க்_குறிப்பு"), + ("Password", "கடவுச்சொல்"), ("Ready", "தயார்"), ("Established", "நிறைவேற்றம்"), ("connecting_status", "இணைப்பு நிலை"), @@ -13,28 +13,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "சேவை இயங்குகிறது"), ("Service is not running", "சேவை இயங்கவில்லை."), ("not_ready_status", "இயக்கம் இல்லை"), - ("Control Remote Desktop", "தொலைதூர டெஸ்க்டாப் கட்டுப்பாடு"), + ("Control Remote Desktop", "ரிமோட் டெஸ்க்டாப் கட்டுப்பாடு"), ("Transfer file", "கோப்பு பரிமாற்றம்"), ("Connect", "இணைக்க"), ("Recent sessions", "கடந்த அமர்வுகள்"), - ("Address book", "முகவர்கள் புல்லிட்டு"), + ("Address book", "முகவரி புத்தகம்"), ("Confirmation", "உறுதிப்படுத்தல்"), ("TCP tunneling", "TCP டன்னலிங்"), ("Remove", "அகற்று"), - ("Refresh random password", "சீரற்ற கடவுச்சொல்லைப் புதுப்பிக்கவும்"), - ("Set your own password", "உங்கள் தனிப்பட்ட கடவுச்சொல்லை அமைக்கவும்"), - ("Enable keyboard/mouse", ""), - ("Enable clipboard", "கிளிப்போர்டை இயக்கு"), - ("Enable file transfer", "கோப்பு பரிமாற்றத்தை இயக்கு"), + ("Refresh random password", "சீரற்ற கடவுச்சொல் புதுப்பி"), + ("Set your own password", "கடவுச்சொல் அமைக்கவும்"), + ("Enable keyboard/mouse", "விசைப்பலகை/சுட்டி இயக்கு"), + ("Enable clipboard", "கிளிப்போர்டு இயக்கு"), + ("Enable file transfer", "கோப்பு பரிமாற்றம் இயக்கு"), ("Enable TCP tunneling", "TCP டன்னலிங் இயக்கு"), - ("IP Whitelisting", "ஐபி அனுமதிப்பட்டியல்"), + ("IP Whitelisting", "IP அனுமதிப்பட்டியல்"), ("ID/Relay Server", "ஐடி/ரிலே சர்வர்"), ("Import server config", "சர்வர் உள்ளமைவு இறக்குமதி"), ("Export Server Config", "சர்வர் உள்ளமைவு ஏற்றுமதி"), - ("Import server configuration successfully", "சர்வர் உள்ளமைவை வெற்றிகரமாக இறக்குமதி செய்யபட்டது"), - ("Export server configuration successfully", "சர்வர் உள்ளமைவை வெற்றிகரமாக ஏற்றுமதி செய்யபட்டது"), - ("Invalid server configuration", "தவறான சர்வர் கட்டமைப்பு"), - ("Clipboard is empty", "கிளிப்போர்டு காலியாக உள்ளது."), + ("Import server configuration successfully", "சர்வர் உள்ளமைவு இறக்குமதி வெற்றி"), + ("Export server configuration successfully", "சர்வர் உள்ளமைவு ஏற்றுமதி வெற்றி"), + ("Invalid server configuration", "தவறான சர்வர் உள்ளமைவு"), + ("Clipboard is empty", "கிளிப்போர்டு காலி"), ("Stop service", "சேவையை நிறுத்து"), ("Change ID", "ஐடி மாற்று"), ("Your new ID", "உங்கள் புதிய ஐடி"), @@ -44,7 +44,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("id_change_tip", "ஐடி_மாற்ற_குறிப்பு"), ("Website", "இணையதளம்"), ("About", "பற்றி"), - ("Slogan_tip", "முழக்கம்_குறிப்பு"), + ("Slogan_tip", "சுலோகம்_குறிப்பு"), ("Privacy Statement", "தனியுரிமை அறிக்கை"), ("Mute", "ஒலியடக்கவும்"), ("Build Date", "கட்டப்பட்ட தேதி"), @@ -77,16 +77,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Error", "பிழை"), ("Reset by the peer", "பியர் மூலம் மீட்டமை"), ("Connecting...", "இணைப்பு ..."), - ("Connection in progress. Please wait.", "இணைப்பு முயற்சிக்கின்றது. நீங்கள் காத்திருக்கவும்..."), - ("Please try 1 minute later", "1 நிமிடம் கழித்து முயற்சிக்கவும்."), + ("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", "மாற்றப்பட்டது"), ("Size", "அளவு"), - ("Show Hidden Files", "முடக்கப்பட்ட கோப்புகளை காட்டு"), + ("Show Hidden Files", "மறைந்த கோப்புகளை காட்டு"), ("Receive", "பெறு"), ("Send", "அனுப்பு"), ("Refresh File", "கோப்பு புதுப்பி"), @@ -102,11 +102,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 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!", "இது மீளாது!"), ("Deleting", "நீக்குதல்"), ("files", "கோப்புகள்"), ("Waiting", "காத்திருக்கும்"), @@ -115,8 +115,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom Image Quality", "தனிப்பட்ட புகைப்பட தரம்"), ("Privacy mode", "தனியுரிமை முறை"), ("Block user input", "பயனர் உள்ளீட்டைத் தடு"), - ("Unblock user input", "பயனர் உள்ளீட்டைத் தடைநீக்கு."), - ("Adjust Window", "சட்டகம் சரிசெய்யவும்"), + ("Unblock user input", "பயனர் உள்ளீடு தடை நீக்கு"), + ("Adjust Window", "சாளரம் சரிசெய்"), ("Original", "அசல்"), ("Shrink", "குறுக்கு"), ("Stretch", "நீட்டு"), @@ -126,29 +126,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Balanced", "சமநிலை"), ("Optimize reaction time", "எதிர்வினை நேரத்தை மேம்பாடு"), ("Custom", "தனிப்பட்ட"), - ("Show remote cursor", "தொலைதூர கற்புக்குழுயை காட்டு"), - ("Show quality monitor", "புகைப்பட தரம் காட்டு"), + ("Show remote cursor", "ரிமோட் கர்சர் காட்டு"), + ("Show quality monitor", "தரம் காட்டு"), ("Disable clipboard", "கிளிப்போர்டை மறை"), ("Lock after session end", "அமர்வு முடிவுக்குப் பின் மறை"), ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del செய்"), ("Insert Lock", "மறை செய்"), ("Refresh", "புதுப்பி"), ("ID does not exist", "ஐடி இல்லை"), - ("Failed to connect to rendezvous server", "ரென்ட்வெர்ச் சர்வருக்கு இணைப்பு பிழை"), - ("Please try later", "பிறகு முயற்சிக்கவும்"), - ("Remote desktop is offline", "தொலைநிலை டெஸ்க்டாப் ஆஃப்லைனில் உள்ளது"), + ("Failed to connect to rendezvous server", "சந்திப்பு சர்வர் இணைப்பு பிழை"), + ("Please try later", "பிறகு முயலவும்"), + ("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", "கடவுச்சொல்லை அமைக்கவும்"), + ("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", "கடவுச்சொல் அமை"), ("OS Password", "OS கடவுச்சொல்"), - ("install_tip", "நிறுவல்_குறிப்பு"), - ("Click to upgrade", "மேம்படுத்த கிளிக் செய்யவும்"), - ("Click to download", "பதிவிறக்க கிளிக் செய்யவும்"), - ("Click to update", "புதுப்பிக்க கிளிக் செய்யவும்"), + ("install_tip", "நிறுவு_குறிப்பு"), + ("Click to upgrade", "மேம்படுத்த கிளிக் செய்"), + ("Click to download", "பதிவிறக்க கிளிக் செய்"), + ("Click to update", "புதுப்பிக்க கிளிக் செய்"), ("Configure", "உள்ளமை"), ("config_acc", "உள்ளமைவு_அக்கெஸ்ஸ்"), ("config_screen", "config_screen"), @@ -156,14 +156,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Install", "நிறுவு"), ("Installation", "நிறுவல்"), ("Installation Path", "நிறுவல் பாதை"), - ("Create start menu shortcuts", "தொடக்க மெனு ஷார்ட்கட்களை உருவாக்கவும்"), - ("Create desktop icon", "டெஸ்க்டாப் ஐகானை உருவாக்கு"), + ("Create start menu shortcuts", "தொடக்க மெனு ஷார்ட்கட் உருவாக்கு"), + ("Create desktop icon", "டெஸ்க்டாப் ஐகான் உருவாக்கு"), ("agreement_tip", "ஒப்பந்தம்_குறிப்பு"), ("Accept and Install", "ஏற்றுக்கொண்டு நிறுவு"), ("End-user license agreement", "இறுதி-பயனர் உரிம ஒப்பந்தம்"), ("Generating ...", "உருவாக்குதல் ..."), - ("Your installation is lower version.", "உங்கள் நிறுவல் கீழ் பதிப்பில் உள்ளது."), - ("not_close_tcp_tip", "not_close_tcp_tip_குறிப்பு"), + ("Your installation is lower version.", "குறைந்த பதிப்பு நிறுவப்பட்டுள்ளது"), + ("not_close_tcp_tip", "tcp_மூடாதே_குறிப்பு"), ("Listening ...", "கேட்கிறது..."), ("Remote Host", "தொலை ஹோஸ்ட்"), ("Remote Port", "தொலை போர்ட்"), @@ -172,9 +172,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "உள்ளூர் போர்ட்"), ("Local Address", "உள்ளூர் முகவரி"), ("Change Local Port", "உள்ளூர் போர்ட் மாற்று"), - ("setup_server_tip", "setup_server_tip_குறிப்பு"), - ("Too short, at least 6 characters.", "மிகக் குறுகியது, குறைந்தபட்சம் 6 எழுத்துக்கள்."), - ("The confirmation is not identical.", "உறுதிப்படுத்தல் ஒரே மாதிரியாக இல்லை."), + ("setup_server_tip", "சர்வர்_அமைவு_குறிப்பு"), + ("Too short, at least 6 characters.", "மிகக் குறுகியது, குறைந்தது 6 எழுத்து"), + ("The confirmation is not identical.", "உறுதிப்படுத்தல் பொருந்தவில்லை"), ("Permissions", "அனுமதிகள்"), ("Accept", "ஏற்று"), ("Dismiss", "ரத்து"), @@ -199,7 +199,7 @@ 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 எதிர்பார்க்கப்படுகிறது"), @@ -212,7 +212,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Run without install", "நிறுவல் இல்லாமல் இயக்கு"), ("Connect via relay", "ரிலே மூலம் இணைக்கவும்"), ("Always connect via relay", "எப்போதும் ரிலே மூலம் இணைக்கவும்"), - ("whitelist_tip", "அனுமதிப்பட்டியல்_குறிப்பு"), + ("whitelist_tip", "வெள்ளைப்பட்டியல்_குறிப்பு"), ("Login", "உள்நுழை"), ("Verify", "உறுதிப்படுத்து"), ("Remember me", "நினைவு கொள்"), @@ -230,7 +230,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "பயனர்பெயர் தவறவிட்டது"), ("Password missed", "கடவுச்சொல் தவறவிட்டது"), ("Wrong credentials", "தவறான சான்றுகள்"), - ("The verification code is incorrect or has expired", "சரிபார்ப்புக் குறியீடு தவறானது அல்லது காலாவதியானது"), + ("The verification code is incorrect or has expired", "சரிபார்ப்புக் குறியீடு தவறானது அல்லது காலாவதி"), ("Edit Tag", "குறிச்சொற்கள் மாற்று"), ("Forget Password", "கடவுச்சொல்லை மறந்துவிடு"), ("Favorites", "விருப்பங்கள்"), @@ -241,464 +241,464 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Socks5 Proxy", "Socks5 ப்ராக்ஸி"), ("Socks5/Http(s) Proxy", "Socks5/Http(s) ப்ராக்ஸி"), ("Discovered", "கண்டுபிடிக்கப்பட்டது"), - ("install_daemon_tip", "install_daemon_tip_குறிப்பு"), + ("install_daemon_tip", "டீமான்_நிறுவு_குறிப்பு"), ("Remote ID", "தொலை ஐடி"), ("Paste", "பேஸ்ட்"), ("Paste here?", "இங்கே பேஸ்ட் செய்?"), - ("Are you sure to close the connection?", "இணைப்பை மூடுவதை உறுதிசெய்?"), + ("Are you sure to close the connection?", "இணைப்பை மூட உறுதியா?"), ("Download new version", "புதிய பதிப்பை பதிவிறக்கு"), - ("Touch mode", ""), - ("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", ""), - ("Reset canvas", ""), - ("No permission of file transfer", ""), - ("Note", ""), - ("Connection", ""), - ("Share screen", ""), - ("Chat", ""), - ("Total", ""), - ("items", ""), - ("Selected", ""), - ("Screen Capture", ""), - ("Input Control", ""), - ("Audio Capture", ""), - ("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", ""), - ("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", ""), - ("Exit Fullscreen", ""), - ("Fullscreen", ""), - ("Mobile Actions", ""), - ("Select Monitor", ""), - ("Control Actions", ""), - ("Display Settings", ""), - ("Ratio", ""), - ("Image Quality", ""), - ("Scroll Style", ""), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), - ("Direct Connection", ""), - ("Relay Connection", ""), - ("Secure Connection", ""), - ("Insecure Connection", ""), - ("Scale original", ""), - ("Scale adaptive", ""), - ("General", ""), - ("Security", ""), - ("Theme", ""), - ("Dark Theme", ""), - ("Light Theme", ""), - ("Dark", ""), - ("Light", ""), - ("Follow System", ""), - ("Enable hardware codec", ""), - ("Unlock Security Settings", ""), - ("Enable audio", ""), - ("Unlock Network Settings", ""), - ("Server", ""), - ("Direct IP Access", ""), - ("Proxy", ""), - ("Apply", ""), - ("Disconnect all devices?", ""), - ("Clear", ""), - ("Audio Input Device", ""), - ("Use IP Whitelisting", ""), - ("Network", ""), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), - ("Recording", ""), - ("Directory", ""), - ("Automatically record incoming sessions", ""), - ("Automatically record outgoing 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...", ""), - ("elevated_foreground_window_tip", ""), - ("Disconnected", ""), - ("Other", ""), - ("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).", ""), - ("Show 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...", ""), - ("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", ""), - ("Right click to select tabs", ""), - ("Skipped", ""), - ("Add to address book", ""), - ("Group", ""), - ("Search", ""), - ("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", ""), - ("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", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), - ("Accessible devices", ""), - ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("view_camera_unsupported_tip", ""), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incoming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), - ("Take screenshot", ""), - ("Taking screenshot", ""), - ("screenshot-merged-screen-not-supported-tip", ""), - ("screenshot-action-tip", ""), - ("Save as", ""), - ("Copy to clipboard", ""), - ("Enable remote printer", ""), - ("Downloading {}", ""), - ("{} Update", ""), - ("{}-to-update-tip", ""), - ("download-new-version-failed-tip", ""), - ("Auto update", ""), - ("update-failed-check-msi-tip", ""), - ("websocket_tip", ""), - ("Use WebSocket", ""), - ("Trackpad speed", ""), - ("Default trackpad speed", ""), - ("Numeric one-time password", ""), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), - ("View camera", ""), - ("Enable camera", ""), - ("No cameras", ""), + ("Touch mode", "தொடுதல் முறை"), + ("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", "கேன்வாஸ் பெரிதாக்கல்"), + ("Reset canvas", "கேன்வாஸ் மீட்டமை"), + ("No permission of file transfer", "கோப்பு பரிமாற்ற அனுமதி இல்லை"), + ("Note", "குறிப்பு"), + ("Connection", "இணைப்பு"), + ("Share Screen", "திரை பகிர்வு"), + ("Chat", "அரட்டை"), + ("Total", "மொத்தம்"), + ("items", "பொருட்கள்"), + ("Selected", "தேர்ந்தெடுக்கப்பட்டது"), + ("Screen Capture", "திரை பிடிப்பு"), + ("Input Control", "உள்ளீடு கட்டுப்பாடு"), + ("Audio Capture", "ஒலி பிடிப்பு"), + ("Do you accept?", "நீங்கள் ஏற்றுக்கொள்கிறீர்களா?"), + ("Open System Setting", "சிஸ்டம் அமைப்புகளைத் திற"), + ("How to get Android input permission?", "Android உள்ளீடு அனுமதி எப்படி பெறுவது?"), + ("android_input_permission_tip1", "RustDesk இந்த Android சாதனத்தை கட்டுப்படுத்த \"அணுகல் சேவைகள்\" அனுமதி தேவை."), + ("android_input_permission_tip2", "சிஸ்டம் அமைப்புகளில் [நிறுவப்பட்ட சேவைகள்] கண்டுபிடித்து, RustDesk சேவை இயக்கவும்."), + ("android_new_connection_tip", "புதிய கட்டுப்பாட்டு கோரிக்கை வந்துள்ளது"), + ("android_service_will_start_tip", "திரை பிடிப்பு இயக்கினால் சேவை தானாக தொடங்கும்"), + ("android_stop_service_tip", "சேவை நிறுத்தினால் எல்லா இணைப்புகளும் மூடிவிடும்"), + ("android_version_audio_tip", "Android 10+ தேவை ஒலி பிடிப்புக்கு"), + ("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", "RustDesk பின்புல சேவையை வைத்திரு"), + ("Ignore Battery Optimizations", "பேட்டரி மேம்படுத்தல்களை புறக்கணி"), + ("android_open_battery_optimizations_tip", "RustDesk க்கு பேட்டரி மேம்படுத்தல் அணைக்க அமைப்புகளுக்கு செல்லவும்"), + ("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", "தொலைநிலை சாதனத்தை மறுதொடக்கு"), + ("Are you sure you want to restart", "மறுதொடக்கம் செய்ய உறுதியா"), + ("Restarting remote device", "ரிமோட் சாதனம் மறுதொடக்கம் ஆகிறது"), + ("remote_restarting_tip", "ரிமோட்_மறுதொடக்கம்_குறிப்பு"), + ("Copied", "நகலெடுக்கப்பட்டது"), + ("Exit Fullscreen", "முழுத்திரையிலிருந்து வெளியேறு"), + ("Fullscreen", "முழுத்திரை"), + ("Mobile Actions", "மொபைல் செயல்கள்"), + ("Select Monitor", "மானிட்டரைத் தேர்ந்தெடு"), + ("Control Actions", "கட்டுப்பாட்டு செயல்கள்"), + ("Display Settings", "திரை அமைப்புகள்"), + ("Ratio", "விகிதம்"), + ("Image Quality", "புகைப்பட தரம்"), + ("Scroll Style", "ஸ்க்ரோல் பாணி"), + ("Show Toolbar", "கருவிப்பட்டியைக் காட்டு"), + ("Hide Toolbar", "கருவிப்பட்டியை மறை"), + ("Direct Connection", "நேரடி இணைப்பு"), + ("Relay Connection", "ரிலே இணைப்பு"), + ("Secure Connection", "பாதுகாப்பான இணைப்பு"), + ("Insecure Connection", "பாதுகாப்பற்ற இணைப்பு"), + ("Scale original", "அசல் அளவு"), + ("Scale adaptive", "தகவமைப்பு அளவு"), + ("General", "பொது"), + ("Security", "பாதுகாப்பு"), + ("Theme", "தீம்"), + ("Dark Theme", "இருண்ட தீம்"), + ("Light Theme", "வெளிச்ச தீம்"), + ("Dark", "இருண்ட"), + ("Light", "வெளிச்சம்"), + ("Follow System", "சிஸ்டத்தைப் பின்பற்று"), + ("Enable hardware codec", "வன்பொருள் கோடெக்கை இயக்கு"), + ("Unlock Security Settings", "பாதுகாப்பு அமைப்புகளை திற"), + ("Enable audio", "ஒலியை இயக்கு"), + ("Unlock Network Settings", "நெட்வொர்க் அமைப்புகளை திற"), + ("Server", "சர்வர்"), + ("Direct IP Access", "நேரடி IP அணுகல்"), + ("Proxy", "ப்ராக்ஸி"), + ("Apply", "பயன்படுத்து"), + ("Disconnect all devices?", "அனைத்து சாதனங்களையும் துண்டிக்கவா?"), + ("Clear", "தெளிவுப்படுத்து"), + ("Audio Input Device", "ஒலி உள்ளீடு சாதனம்"), + ("Use IP Whitelisting", "IP அனுமதிப்பட்டியலைப் பயன்படுத்து"), + ("Network", "நெட்வொர்க்"), + ("Pin Toolbar", "கருவிப்பட்டியை பின் செய்"), + ("Unpin Toolbar", "கருவிப்பட்டியை அன்பின் செய்"), + ("Recording", "பதிவு"), + ("Directory", "கோப்பகம்"), + ("Automatically record incoming sessions", "உள்வரும் அமர்வுகளை தானாக பதிவு செய்"), + ("Automatically record outgoing sessions", "வெளியேறும் அமர்வுகளை தானாக பதிவு செய்"), + ("Change", "மாற்று"), + ("Start session recording", "அமர்வு பதிவைத் தொடங்கு"), + ("Stop session recording", "அமர்வு பதிவை நிறுத்து"), + ("Enable recording session", "பதிவு அமர்வை இயக்கு"), + ("Enable LAN discovery", "LAN கண்டுபிடிப்பை இயக்கு"), + ("Deny LAN discovery", "LAN கண்டுபிடிப்பை மறு"), + ("Write a message", "ஒரு செய்தி எழுது"), + ("Prompt", "தூண்டுதல்"), + ("Please wait for confirmation of UAC...", "UAC உறுதிப்படுத்தலுக்காக காத்திருக்கவும்..."), + ("elevated_foreground_window_tip", "முன்னணி_சாளர_உயர்வு_குறிப்பு"), + ("Disconnected", "துண்டிக்கப்பட்டது"), + ("Other", "மற்றவை"), + ("Confirm before closing multiple tabs", "பல தாவல்களை மூடுவதற்கு முன் உறுதிப்படுத்து"), + ("Keyboard Settings", "விசைப்பலகை அமைப்புகள்"), + ("Full Access", "முழு அணுகல்"), + ("Screen Share", "திரை பகிர்வு"), + ("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 முயற்சிக்கவும் அல்லது OS மாற்றவும்."), + ("JumpLink", "ஜம்ப் லிங்க்"), + ("Please Select the screen to be shared(Operate on the peer side).", "பகிரப்பட வேண்டிய திரை தேர்ந்தெடுக்கவும்"), + ("Show RustDesk", "RustDesk ஐ காட்டு"), + ("This PC", "இந்த 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...", "அமர்வு கோரிக்கை ஏற்பதற்காக காத்திருக்கவும்..."), + ("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", "வேலேண்ட்_சோதனை_குறிப்பு"), + ("Right click to select tabs", "தாவல்களைத் தேர்ந்தெடுக்க வலது கிளிக் செய்யவும்"), + ("Skipped", "தவிர்க்கப்பட்டது"), + ("Add to address book", "முகவரி புத்தகத்தில் சேர்"), + ("Group", "குழு"), + ("Search", "தேடு"), + ("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", "uac_ஐ_இன்னும்_சொடுக்கவும்_குறிப்பு"), + ("Request Elevation", "உயர்வு கோரிக்கை"), + ("wait_accept_uac_tip", "uac_ஏற்புக்காக_காத்திரு_குறிப்பு"), + ("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", "பிட்ரேட்"), + ("FPS", "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", "RDP அமைப்புகள்"), + ("Sort by", "வரிசைப்படுத்து"), + ("New Connection", "புதிய இணைப்பு"), + ("Restore", "மீட்டமை"), + ("Minimize", "குறைக்கவும்"), + ("Maximize", "பெரிதாக்கு"), + ("Your Device", "உங்கள் சாதனம்"), + ("empty_recent_tip", "காலி_சமீபத்திய_குறிப்பு"), + ("empty_favorite_tip", "காலி_விருப்பமான_குறிப்பு"), + ("empty_lan_tip", "காலி_லேன்_குறிப்பு"), + ("empty_address_book_tip", "காலி_முகவரி_புத்தக_குறிப்பு"), + ("eg: admin", "எ.கா: admin"), + ("Empty Username", "காலி பயனர்பெயர்"), + ("Empty Password", "காலி கடவுச்சொல்"), + ("Me", "நான்"), + ("identical_file_tip", "ஒரே_மாதிரியான_கோப்பு_குறிப்பு"), + ("show_monitors_tip", "மானிட்டர்களை_காட்டு_குறிப்பு"), + ("View Mode", "காட்சி முறை"), + ("login_linux_tip", "லினக்ஸ்_உள்நுழைவு_குறிப்பு"), + ("verify_rustdesk_password_tip", "rustdesk_கடவுச்சொல்_சரிபார்ப்பு_குறிப்பு"), + ("remember_account_tip", "கணக்கை_நினைவில்_கொள்_குறிப்பு"), + ("os_account_desk_tip", "os_கணக்கு_டெஸ்க்_குறிப்பு"), + ("OS Account", "OS கணக்கு"), + ("another_user_login_title_tip", "மற்றொரு_பயனர்_உள்நுழைவு_தலைப்பு_குறிப்பு"), + ("another_user_login_text_tip", "மற்றொரு_பயனர்_உள்நுழைவு_உரை_குறிப்பு"), + ("xorg_not_found_title_tip", "xorg_காணப்படவில்லை_தலைப்பு_குறிப்பு"), + ("xorg_not_found_text_tip", "xorg_காணப்படவில்லை_உரை_குறிப்பு"), + ("no_desktop_title_tip", "டெஸ்க்டாப்_இல்லை_தலைப்பு_குறிப்பு"), + ("no_desktop_text_tip", "டெஸ்க்டாப்_இல்லை_உரை_குறிப்பு"), + ("No need to elevate", "உயர்த்த தேவையில்லை"), + ("System Sound", "சிஸ்டம் ஒலி"), + ("Default", "இயல்புநிலை"), + ("New RDP", "புதிய 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", "HSV நிறம்"), + ("Installation Successful!", "நிறுவல் வெற்றி!"), + ("Installation failed!", "நிறுவல் தோல்வி!"), + ("Reverse mouse wheel", "சுட்டி சக்கரம் தலைகீழ்"), + ("{} sessions", "{} அமர்வுகள்"), + ("scam_title", "மோசடி எச்சரிக்கை"), + ("scam_text1", "தொலைபேசி மோசடியின் பலியாகலாம்!"), + ("scam_text2", "RustDesk ஊழியர் இவ்வாறு தொடர்பு கொள்ள மாட்டார்கள்"), + ("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", "RustDesk Server Pro {} க்கு மேம்படுத்து"), + ("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", "SELinux இயக்கப்பட்டது, RustDesk அனுமதி வேண்டும்"), + ("Change view", "காட்சி மாற்று"), + ("Big tiles", "பெரிய ஓடுகள்"), + ("Small tiles", "சிறிய ஓடுகள்"), + ("List", "பட்டியல்"), + ("Virtual display", "மெய்நிகர் காட்சி"), + ("Plug out all", "அனைத்தையும் துண்டி"), + ("True color (4:4:4)", "உண்மை நிறம் (4:4:4)"), + ("Enable blocking user input", "பயனர் உள்ளீடு தடுப்பு இயக்கு"), + ("id_input_tip", "ஐடி உள்ளீடு எழுத்துகள் எண்கள் மட்டும்"), + ("privacy_mode_impl_mag_tip", "Windows Magnifier API"), + ("privacy_mode_impl_virtual_display_tip", "Virtual Display Driver"), + ("Enter privacy mode", "தனியுரிமை முறையில் நுழை"), + ("Exit privacy mode", "தனியுரிமை முறையிலிருந்து வெளியேறு"), + ("idd_not_support_under_win10_2004_tip", "Virtual Display Driver Windows 10 2004 க்கு கீழ் ஆதரவில்லை"), + ("input_source_1_tip", "உள்ளீடு மூலம் = விசைப்பலகை"), + ("input_source_2_tip", "உள்ளீடு மூலம் = சுட்டி"), + ("Swap control-command key", "control-command விசை மாற்று"), + ("swap-left-right-mouse", "இடது-வலது சுட்டி மாற்று"), + ("2FA code", "2FA குறியீடு"), + ("More", "மேலும்"), + ("enable-2fa-title", "இரு காரணி அங்கீகாரம் இயக்கு"), + ("enable-2fa-desc", "RustDesk இரு காரணி அங்கீகாரம் ஆதரிக்கிறது"), + ("wrong-2fa-code", "தவறான 2FA குறியீடு"), + ("enter-2fa-title", "2FA குறியீடு உள்ளிடு"), + ("Email verification code must be 6 characters.", "மின்னஞ்சல் சரிபார்ப்பு 6 எழுத்துகள்"), + ("2FA code must be 6 digits.", "2FA குறியீடு 6 எண்கள்"), + ("Multiple Windows sessions found", "பல Windows அமர்வுகள் கண்டறியப்பட்டன"), + ("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", "Wayland திரை தேர்வு அழி"), + ("clear_Wayland_screen_selection_tip", "wayland_திரை_தேர்வு_அழி_குறிப்பு"), + ("confirm_clear_Wayland_screen_selection_tip", "wayland_திரை_தேர்வு_அழிக்க_உறுதிப்படுத்து_குறிப்பு"), + ("android_new_voice_call_tip", "android_புதிய_குரல்_அழைப்பு_குறிப்பு"), + ("texture_render_tip", "டெக்ஸ்ச்சர்_ரெண்டர்_குறிப்பு"), + ("Use texture rendering", "texture ரெண்டரிங் பயன்படுத்து"), + ("Floating window", "மிதக்கும் சாளரம்"), + ("floating_window_tip", "மிதக்கும்_சாளரம்_குறிப்பு"), + ("Keep screen on", "திரை இயக்கத்தில் வை"), + ("Never", "ஒருபோதும் இல்லை"), + ("During controlled", "கட்டுப்படுத்தும்போது"), + ("During service is on", "சேவை இயக்கத்தில் இருக்கும்போது"), + ("Capture screen using DirectX", "DirectX பயன்படுத்தி திரை பிடிப்பு"), + ("Back", "பின்"), + ("Apps", "ஆப்ஸ்"), + ("Volume up", "ஒலி அதிகரி"), + ("Volume down", "ஒலி குறை"), + ("Power", "மின் பட்டன்"), + ("Telegram bot", "Telegram போட்"), + ("enable-bot-tip", "போட்_இயக்க_குறிப்பு"), + ("enable-bot-desc", "RustDesk Telegram போட் ஆதரிக்கிறது"), + ("cancel-2fa-confirm-tip", "2fa_ரத்து_உறுதி_குறிப்பு"), + ("cancel-bot-confirm-tip", "போட்_ரத்து_உறுதி_குறிப்பு"), + ("About RustDesk", "RustDesk பற்றி"), + ("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", "நம்பகமான_சாதனங்கள்_இயக்க_குறிப்பு"), + ("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", "கிளிப்போர்டு ஒத்திசைக்கப்பட்டது"), + ("Update client clipboard", "கிளையன் கிளிப்போர்டு புதுப்பி"), + ("Untagged", "குறிச்சொல் இல்லாத"), + ("new-version-of-{}-tip", "{}_புதிய_பதிப்பு_குறிப்பு"), + ("Accessible devices", "அணுகக்கூடிய சாதனங்கள்"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "ரிமோட்_rustdesk_கிளையன்டை_{}_மேம்படுத்து_குறிப்பு"), + ("view_camera_unsupported_tip", "கேமரா_காட்சி_ஆதரவற்ற_குறிப்பு"), + ("d3d_render_tip", "d3d_ரெண்டர்_குறிப்பு"), + ("Use D3D rendering", "D3D ரெண்டரிங் பயன்படுத்து"), + ("Printer", "அச்சுப்பொறி"), + ("printer-os-requirement-tip", "பிரிண்டர்_os_தேவை_குறிப்பு"), + ("printer-requires-installed-{}-client-tip", "பிரிண்டர்_தேவை_நிறுவப்பட்ட_{}_கிளையண்ட்_குறிப்பு"), + ("printer-{}-not-installed-tip", "பிரிண்டர்_{}_நிறுவப்படவில்லை_குறிப்பு"), + ("printer-{}-ready-tip", "பிரிண்டர்_{}_தயார்_குறிப்பு"), + ("Install {} Printer", "{} அச்சுப்பொறி நிறுவு"), + ("Outgoing Print Jobs", "வெளியேறும் அச்சு வேலைகள்"), + ("Incoming Print Jobs", "உள்வரும் அச்சு வேலைகள்"), + ("Incoming Print Job", "உள்வரும் அச்சு வேலை"), + ("use-the-default-printer-tip", "இயல்புநிலை_அச்சுப்பொறியை_பயன்படுத்து_குறிப்பு"), + ("use-the-selected-printer-tip", "தேர்ந்தெடுக்கப்பட்ட_அச்சுப்பொறியை_பயன்படுத்து_குறிப்பு"), + ("auto-print-tip", "தானியங்கு_அச்சு_குறிப்பு"), + ("print-incoming-job-confirm-tip", "உள்வரும்_அச்சு_வேலையை_உறுதிப்படுத்து_குறிப்பு"), + ("remote-printing-disallowed-tile-tip", "ரிமோட்_அச்சிடுதல்_அனுமதிக்கப்படாத_டைல்_குறிப்பு"), + ("remote-printing-disallowed-text-tip", "ரிமோட்_அச்சிடுதல்_அனுமதிக்கப்படாத_உரை_குறிப்பு"), + ("save-settings-tip", "அமைப்புகளை_சேமி_குறிப்பு"), + ("dont-show-again-tip", "மீண்டும்_காட்டாதே_குறிப்பு"), + ("Take screenshot", "திரைப்பிடிப்பு எடு"), + ("Taking screenshot", "திரைப்பிடிப்பு எடுத்துக்கொண்டிருக்கிறது"), + ("screenshot-merged-screen-not-supported-tip", "ஸ்கிரீன்ஷாட்_இணைக்கப்பட்ட_திரை_ஆதரவற்ற_குறிப்பு"), + ("screenshot-action-tip", "ஸ்கிரீன்ஷாட்_செயல்_குறிப்பு"), + ("Save as", "இப்படி சேமி"), + ("Copy to clipboard", "கிளிப்போர்டில் நகல்"), + ("Enable remote printer", "தொலை அச்சுப்பொறி இயக்கு"), + ("Downloading {}", "{} பதிவிறக்குகிறது"), + ("{} Update", "{} புதுப்பிப்பு"), + ("{}-to-update-tip", "{}_புதுப்பிக்க_குறிப்பு"), + ("download-new-version-failed-tip", "புதிய_பதிப்பு_பதிவிறக்கம்_தோல்வி_குறிப்பு"), + ("Auto update", "தானியங்கு புதுப்பிப்பு"), + ("update-failed-check-msi-tip", "புதுப்பிப்பு_தோல்வி_எம்எஸ்ஐ_சரிபார்_குறிப்பு"), + ("websocket_tip", "வெப்சாக்கெட்_குறிப்பு"), + ("Use WebSocket", "WebSocket பயன்படுத்து"), + ("Trackpad speed", "டிராக்பேட் வேகம்"), + ("Default trackpad speed", "இயல்புநிலை டிராக்பேட் வேகம்"), + ("Numeric one-time password", "எண் ஒருமுறை கடவுச்சொல்"), + ("Enable IPv6 P2P connection", "IPv6 P2P இணைப்பு இயக்கு"), + ("Enable UDP hole punching", "UDP hole punching இயக்கு"), + ("View camera", "கேமரா பார்"), + ("Enable camera", "கேமரா இயக்கு"), + ("No cameras", "கேமராக்கள் இல்லை"), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), From 7ce13a21f87695e42e7d604992ffc097e07816ad Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 1 Jul 2025 13:23:59 +0800 Subject: [PATCH 382/506] reorder lang/template.rs --- 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/ge.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/sc.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/ta.rs | 4 ++-- 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/vi.rs | 2 +- 46 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/lang/ar.rs b/src/lang/ar.rs index bff726e3b97..8da03788f38 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "تحديث جديد متاح لـ {}"), ("Accessible devices", "الأجهزة القابلة للوصول"), ("upgrade_remote_rustdesk_client_to_{}_tip", "ترقية عميل RustDesk البعيد إلى {}"), - ("view_camera_unsupported_tip", "عرض الكاميرا غير مدعوم في هذا الجهاز"), ("d3d_render_tip", "تمكين العرض باستخدام D3D"), ("Use D3D rendering", "استخدام عرض D3D"), ("Printer", "الطابعة"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "عرض الكاميرا"), ("Enable camera", "تمكين الكاميرا"), ("No cameras", "لا توجد كاميرات"), + ("view_camera_unsupported_tip", "عرض الكاميرا غير مدعوم في هذا الجهاز"), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/be.rs b/src/lang/be.rs index 79eb530c413..85c424cae80 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Калі ласка, абнавіце кліент RustDesk да версіі {} або навейшай на аддаленым баку!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Прагляд камеры"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 1d90a83e211..1bf1a48456d 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Моля, надстройте клиента RustDesk до версия {} или по-нова от отдалечената страна!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Преглед на камерата"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/ca.rs b/src/lang/ca.rs index c4d88fa68d7..c6ece9ec168 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", "Dispositius accessibles"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre à niveau le client RustDesk vers la version {} ou plus récente du côté distant !"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Mostra la càmera"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 974d2a99268..f7110573f42 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "{} 版本更新"), ("Accessible devices", "可访问的设备"), ("upgrade_remote_rustdesk_client_to_{}_tip", "请在远程端将 RustDesk 客户端升级至版本 {} 或更新版本!"), - ("view_camera_unsupported_tip", "您的远程端不支持查看摄像头。"), ("d3d_render_tip", "当启用 D3D 渲染时,某些机器可能无法显示远程画面。"), ("Use D3D rendering", "使用 D3D 渲染"), ("Printer", "打印机"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "查看摄像头"), ("Enable camera", "允许查看摄像头"), ("No cameras", "没有摄像头"), + ("view_camera_unsupported_tip", "您的远程端不支持查看摄像头。"), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 2b99704b329..1b5a0b49230 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgradujte prosím klienta RustDesk na verzi {} nebo novější na vzdálené straně!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Zobrazit kameru"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index 52641769b6a..0aab88c9c02 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Opgrader venligst RustDesk-klienten til version {} eller nyere på fjernsiden!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Se kamera"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 0db57aa7a81..9b68d282170 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"), ("Accessible devices", "Erreichbare Geräte"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Bitte aktualisieren Sie den RustDesk-Client auf der Remote-Seite auf Version {} oder neuer!"), - ("view_camera_unsupported_tip", "Das entfernte Gerät kann die Kamera nicht anzeigen."), ("d3d_render_tip", "Wenn das D3D-Rendering aktiviert ist, kann der entfernte Bildschirm auf manchen Rechnern schwarz sein."), ("Use D3D rendering", "D3D-Rendering verwenden"), ("Printer", "Drucker"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Kamera anzeigen"), ("Enable camera", "Kamera zulassen"), ("No cameras", "Keine Kameras"), + ("view_camera_unsupported_tip", "Das entfernte Gerät kann die Kamera nicht anzeigen."), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/el.rs b/src/lang/el.rs index 9f6bcee468d..2accbd9a7b0 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"), ("Accessible devices", "Προσβάσιμες συσκευές"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότερη στην απομακρυσμένη πλευρά!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Προβολή κάμερας"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index b454dc49278..a9dda55e73d 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Rigardi kameron"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index 998c278cf30..ac588e7106c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"), - ("view_camera_unsupported_tip", "El dispositivo remoto no soporta la visualización de la cámara."), ("d3d_render_tip", "Al activar el renderizado D3D, la pantalla de control remoto puede verse negra en algunos equipos."), ("Use D3D rendering", "Usar renderizado D3D"), ("Printer", "Impresora"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Ver cámara"), ("Enable camera", "Habilitar cámara"), ("No cameras", "No hay cámaras"), + ("view_camera_unsupported_tip", "El dispositivo remoto no soporta la visualización de la cámara."), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/et.rs b/src/lang/et.rs index 7928ebe11da..bde37460148 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Saadaval on {} uus versioon"), ("Accessible devices", "Ligipääsetavad seadmed"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Täiendage RustDeski klient kaugküljel versioonile {} või uuemale!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Vaata kaamerat"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/eu.rs b/src/lang/eu.rs index ab59dfc24c6..46f3e8a9b78 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Mesedez, eguneratu RustDesk bezeroa {} bertsiora edo berriagoa urruneko aldean!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Ikusi kamera"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b4fef049bd8..538e0610c07 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "نسخه جدید {} در دسترس است"), ("Accessible devices", "دستگاه‌های در دسترس"), ("upgrade_remote_rustdesk_client_to_{}_tip", "لطفاً RustDesk را به نسخه {} یا جدیدتر در سمت راه دور ارتقا دهید"), - ("view_camera_unsupported_tip", "دوربین در این دستگاه پشتیبانی نمی‌شود"), ("d3d_render_tip", "فعال کردن رندر D3D برای عملکرد بهتر"), ("Use D3D rendering", "استفاده از رندر D3D"), ("Printer", "چاپگر"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "نمایش دوربین"), ("Enable camera", "فعال کردن دوربین"), ("No cameras", "هیچ دوربینی یافت نشد"), + ("view_camera_unsupported_tip", "دوربین در این دستگاه پشتیبانی نمی‌شود"), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 6e0b4f4c76e..d6a273cbe5b 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Une nouvelle version de {} est disponible"), ("Accessible devices", "Appareils accessibles"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre le client RustDesk distant à jour vers la version {} ou ultérieure !"), - ("view_camera_unsupported_tip", "L’appareil distant ne prend pas en charge l’affichage de la caméra."), ("d3d_render_tip", "Sur certaines machines, l’écran du contrôle à distance peut rester noir lors de l’utilisation du rendu D3D."), ("Use D3D rendering", "Utiliser le rendu D3D"), ("Printer", "Imprimante"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Afficher la caméra"), ("Enable camera", "Activer la caméra"), ("No cameras", "Aucune caméra"), + ("view_camera_unsupported_tip", "L’appareil distant ne prend pas en charge l’affichage de la caméra."), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 9d83ff58a7d..6adc2606d2b 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "ხელმისაწვდომია ახალი ვერსია {}"), ("Accessible devices", "ხელმისაწვდომი მოწყობილობები"), ("upgrade_remote_rustdesk_client_to_{}_tip", "განაახლეთ RustDesk კლიენტი ვერსიამდე {} ან უფრო ახალი დისტანციურ მხარეზე!"), - ("view_camera_unsupported_tip", "დისტანციური მოწყობილობა არ უჭერს მხარს კამერის ნახვას."), ("d3d_render_tip", "D3D ვიზუალიზაციის ჩართვისას ზოგიერთ მოწყობილობაზე დისტანციური ეკრანი შეიძლება იყოს შავი."), ("Use D3D rendering", "D3D ვიზუალიზაციის გამოყენება"), ("Printer", "პრინტერი"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "კამერის ნახვა"), ("Enable camera", "კამერის ჩართვა"), ("No cameras", "კამერა არ არის"), + ("view_camera_unsupported_tip", "დისტანციური მოწყობილობა არ უჭერს მხარს კამერის ნახვას."), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/he.rs b/src/lang/he.rs index fc55dd94d2d..af33c8c5fa6 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "גרסה חדשה של {} זמינה"), ("Accessible devices", "מכשירים נגישים"), ("upgrade_remote_rustdesk_client_to_{}_tip", "אנא שדרג את לקוח RustDesk לגרסה {} או חדשה יותר בצד המרוחק!"), - ("view_camera_unsupported_tip", "הצגת מצלמה אינה נתמכת במכשיר המרוחק"), ("d3d_render_tip", "שימוש בעיבוד Direct3D עשוי לשפר ביצועים בחלק מהמקרים"), ("Use D3D rendering", "השתמש בעיבוד D3D"), ("Printer", "מדפסת"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "הצג מצלמה"), ("Enable camera", "הפעל מצלמה"), ("No cameras", "אין מצלמות"), + ("view_camera_unsupported_tip", "הצגת מצלמה אינה נתמכת במכשיר המרוחק"), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/hr.rs b/src/lang/hr.rs index a0ea613c1d5..e1ea1837f35 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Molimo ažurirajte RustDesk klijent na verziju {} ili noviju na udaljenoj strani!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Pregled kamere"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index f4bb24734bc..18c12b17723 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "A(z) {} új verziója"), ("Accessible devices", "Hozzáférhető eszközök"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Frissítse a RustDesk klienst {} vagy újabb verziójára a távoli oldalon!"), - ("view_camera_unsupported_tip", "A kameranézet nem támogatott"), ("d3d_render_tip", "D3D renderelés"), ("Use D3D rendering", "D3D renderelés használata"), ("Printer", "Nyomtató"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Kamera megtekintése"), ("Enable camera", "Kamera engedélyezése"), ("No cameras", "Nincs kamera"), + ("view_camera_unsupported_tip", "A kameranézet nem támogatott"), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/id.rs b/src/lang/id.rs index 8d287dda197..fcc72431df8 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Versi {} sudah tersedia."), ("Accessible devices", "Perangkat yang tersedia"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Silahkan perbarui aplikasi RustDesk ke versi {} atau yang lebih baru pada komputer yang akan terhubung!"), - ("view_camera_unsupported_tip", "Perangkat yang terhubung tidak mendukung tampilan kamera."), ("d3d_render_tip", "Ketika rendering D3D diaktifkan, layar kontrol jarak jauh bisa tampak hitam di beberapa komputer"), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Lihat Kamera"), ("Enable camera", "Aktifkan kamera"), ("No cameras", "Tidak ada kamera"), + ("view_camera_unsupported_tip", "Perangkat yang terhubung tidak mendukung tampilan kamera."), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/it.rs b/src/lang/it.rs index 16c2097c8b2..4a1447f160a 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "È disponibile una nuova versione di {}"), ("Accessible devices", "Dispositivi accessibili"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Aggiorna il client RustDesk remoto alla versione {} o successiva!"), - ("view_camera_unsupported_tip", "Il dispositivo remoto non supporta la visualizzazione della camera."), ("d3d_render_tip", "Quando è abilitato il rendering D3D, in alcuni computer la schermata del telecomando potrebbe essere nera."), ("Use D3D rendering", "Usa rendering D3D"), ("Printer", "Stampante"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Visualizza telecamera"), ("Enable camera", "Abilita camera"), ("No cameras", "Nessuna camera"), + ("view_camera_unsupported_tip", "Il dispositivo remoto non supporta la visualizzazione della camera."), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index efa547095cc..14e322eeba5 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "リモート側のRustDeskクライアントをバージョン{}以上にアップグレードしてください!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "カメラを表示"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 8db1e737c7d..863748e3763 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "{}의 새 버전이 출시되었습니다."), ("Accessible devices", "연결 가능한 기기"), ("upgrade_remote_rustdesk_client_to_{}_tip", "원격 기기의 RustDesk 클라이언트를 {} 버전 이상으로 업그레이드하십시오!"), - ("view_camera_unsupported_tip", "원격 기기에서 카메라 보기를 지원하지 않습니다."), ("d3d_render_tip", "D3D 렌더링을 활성화하면 일부 기기에서 원격 화면이 표시되지 않을 수 있습니다."), ("Use D3D rendering", "D3D 렌더링 활성화"), ("Printer", "프린터"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "카메라 보기"), ("Enable camera", "카메라 보기 허용"), ("No cameras", "카메라 없음"), + ("view_camera_unsupported_tip", "원격 기기에서 카메라 보기를 지원하지 않습니다."), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 79b94db1c90..b02bd1c0db0 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Қашықтағы жақтағы RustDesk клиентін {} немесе одан жоғары нұсқаға жаңартуды өтінеміз!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Камераны Көру"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/lt.rs b/src/lang/lt.rs index be0f69345fa..598a54efd6d 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Prašome atnaujinti nuotolinės pusės RustDesk klientą į {} ar naujesnę versiją!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Peržiūrėti kamerą"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 28766130037..327a4531731 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Ir pieejama jauna {} versija"), ("Accessible devices", "Pieejamas ierīces"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Lūdzu, jauniniet attālās puses RustDesk klientu uz versiju {} vai jaunāku!"), - ("view_camera_unsupported_tip", "Attālā ierīce neatbalsta kameras skatīšanos."), ("d3d_render_tip", "Ja ir iespējota D3D renderēšana, dažās ierīcēs tālvadības pults ekrāns var būt melns."), ("Use D3D rendering", "Izmantot D3D renderēšanu"), ("Printer", "Printeris"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Skatīt kameru"), ("Enable camera", "Iespējot kameru"), ("No cameras", "Nav kameru"), + ("view_camera_unsupported_tip", "Attālā ierīce neatbalsta kameras skatīšanos."), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 47aad365286..c2719c86e67 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Vis kamera"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 516e0db95ef..ba068e712b8 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"), ("Accessible devices", "Toegankelijke apparaten"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgrade de RustDesk client naar versie {} of nieuwer op de externe computer!"), - ("view_camera_unsupported_tip", "Het externe apparaat ondersteunt geen cameraweergave."), ("d3d_render_tip", "Wanneer D3D-rendering is ingeschakeld kan het externe scherm op sommige apparaten, zwart zijn."), ("Use D3D rendering", "Gebruik D3D-rendering"), ("Printer", "Printer"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Camera bekijken"), ("Enable camera", "Camera inschakelen"), ("No cameras", "Geen camera's"), + ("view_camera_unsupported_tip", "Het externe apparaat ondersteunt geen cameraweergave."), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 3086e9dd04c..93b328f0a63 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Dostępna jest nowa wersja {}"), ("Accessible devices", "Dostępne urządzenia"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Proszę zaktualizować zdalny klient RustDesk do wersji {} lub nowszej!"), - ("view_camera_unsupported_tip", "Zdalne urządzenie nie obsługuje podglądu kamery."), ("d3d_render_tip", "Kiedy włączenie renderowania D3D jest włączone, ekran zdalnej kontroli może być czarny w niektórych przypadkach"), ("Use D3D rendering", "Użyj renderowania D3D"), ("Printer", "Drukarka"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Podgląd kamery"), ("Enable camera", "Włącz kamerę"), ("No cameras", "Brak kamer"), + ("view_camera_unsupported_tip", "Zdalne urządzenie nie obsługuje podglądu kamery."), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index c007c3114c7..c4fc7818793 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Ver câmara"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 12199a9ac20..1a715121d30 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Uma nova versão de {} está disponível"), ("Accessible devices", "Dispositivos acessíveis"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualize o cliente RustDesk para a versão {} ou superior no lado remoto."), - ("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."), ("d3d_render_tip", "Em algumas máquinas, a tela do controle remoto pode ficar preta ao usar a renderização D3D."), ("Use D3D rendering", "Usar renderização D3D"), ("Printer", "Impressora"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Visualizar câmera"), ("Enable camera", "Ativar câmera"), ("No cameras", "Sem câmeras"), + ("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 8ff39d53fc0..41d86baab25 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Vezi camera"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index b84161a897b..e5d2e251817 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Доступна новая версия {}"), ("Accessible devices", "Доступные устройства"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Обновите клиент RustDesk до версии {} или новее на удалённой стороне!"), - ("view_camera_unsupported_tip", "Удалённое устройство не поддерживает просмотр камеры."), ("d3d_render_tip", "При включении визуализации D3D на некоторых устройствах удалённый экран может быть чёрным."), ("Use D3D rendering", "Использовать визуализацию D3D"), ("Printer", "Принтер"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Просмотр камеры"), ("Enable camera", "Включить камеру"), ("No cameras", "Камера отсутствует"), + ("view_camera_unsupported_tip", "Удалённое устройство не поддерживает просмотр камеры."), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 9610ca83cc3..14d5b7e0472 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "B'at una versione noa de {} a disponimentu"), ("Accessible devices", "Dispositivos atzessìbiles"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualiza su cliente RustDesk remotu a sa versione {} o prus noa!"), - ("view_camera_unsupported_tip", "Su dispositivu remotu non suportat sa visualizatzione de sa càmera"), ("d3d_render_tip", "Cando sa renderizatzione D3D est abilitada, s'ischermu de controllu remotu diat pòdere èssere nieddu in unas cantas màchinas"), ("Use D3D rendering", "Imprea sa renderizatzione D3D"), ("Printer", "Imprentadora"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Mustra sa càmera"), ("Enable camera", "Abìlita sa càmera"), ("No cameras", "Peruna càmera"), + ("view_camera_unsupported_tip", "Su dispositivu remotu non suportat sa visualizatzione de sa càmera"), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 6f8968ce0f3..439c90d0059 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Aktualizujte klienta RustDesk na verziu {} alebo novšiu na vzdialenej strane!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Zobraziť kameru"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 716c59a0fa3..78551d37d33 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Na voljo je nova različica {}"), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Prosimo, nadgradite RustDesk odjemalec na različico {} ali novejšo na oddaljeni strani."), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Pogled kamere"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index a3107e86753..f8aa4e6b32c 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", ""), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index f39dd00b094..f2be9629f6e 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Pregled kamere"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index e2e739c1f30..ecd6122c576 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Visa kamera"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/ta.rs b/src/lang/ta.rs index e372ce5bac8..27276683140 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -267,7 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "கோப்பு பரிமாற்ற அனுமதி இல்லை"), ("Note", "குறிப்பு"), ("Connection", "இணைப்பு"), - ("Share Screen", "திரை பகிர்வு"), + ("Share screen", ""), ("Chat", "அரட்டை"), ("Total", "மொத்தம்"), ("items", "பொருட்கள்"), @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "{}_புதிய_பதிப்பு_குறிப்பு"), ("Accessible devices", "அணுகக்கூடிய சாதனங்கள்"), ("upgrade_remote_rustdesk_client_to_{}_tip", "ரிமோட்_rustdesk_கிளையன்டை_{}_மேம்படுத்து_குறிப்பு"), - ("view_camera_unsupported_tip", "கேமரா_காட்சி_ஆதரவற்ற_குறிப்பு"), ("d3d_render_tip", "d3d_ரெண்டர்_குறிப்பு"), ("Use D3D rendering", "D3D ரெண்டரிங் பயன்படுத்து"), ("Printer", "அச்சுப்பொறி"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "கேமரா பார்"), ("Enable camera", "கேமரா இயக்கு"), ("No cameras", "கேமராக்கள் இல்லை"), + ("view_camera_unsupported_tip", "கேமரா_காட்சி_ஆதரவற்ற_குறிப்பு"), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/template.rs b/src/lang/template.rs index 6fdd8af21e4..a4934e82e6f 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", ""), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index 1317e7998dd..65774bffc55 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "กรุณาอัปเดต RustDesk ไคลเอนต์ไปยังเวอร์ชัน {} หรือใหม่กว่าที่ฝั่งปลายทาง!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "ดูกล้อง"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 33697efeb6c..8284dcc5107 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Kamerayı görüntüle"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index adc55deb760..b5e44d07aaa 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "有新版本的 {} 可用"), ("Accessible devices", "可存取的裝置"), ("upgrade_remote_rustdesk_client_to_{}_tip", "請將遠端 RustDesk 客戶端升級到 {} 或更新版本!"), - ("view_camera_unsupported_tip", "您的遠端設備不支援查看鏡頭"), ("d3d_render_tip", "當啟用 D3D 渲染時,某些機器可能會無法顯示遠端畫面。"), ("Use D3D rendering", "使用 D3D 渲染"), ("Printer", "印表機"), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "檢視相機"), ("Enable camera", "允許查看鏡頭"), ("No cameras", "沒有鏡頭"), + ("view_camera_unsupported_tip", "您的遠端設備不支援查看鏡頭"), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/uk.rs b/src/lang/uk.rs index c6df72e3e64..7a5103ef3c9 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Доступна нова версія {}"), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", "Будь ласка, оновіть RustDesk клієнт на віддаленому пристрої до версії {} чи новіше!"), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Перегляд камери"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), diff --git a/src/lang/vi.rs b/src/lang/vi.rs index f820766e0c1..332c51d68f8 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -656,7 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", ""), ("Accessible devices", ""), ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("view_camera_unsupported_tip", ""), ("d3d_render_tip", ""), ("Use D3D rendering", ""), ("Printer", ""), @@ -699,6 +698,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "Xem camera"), ("Enable camera", ""), ("No cameras", ""), + ("view_camera_unsupported_tip", ""), ("Terminal", ""), ("Enable terminal", ""), ("New tab", ""), From 09098e86ca306aebfaf778df8876790aeafa2faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?COYG=E2=9A=A1=EF=B8=8F?= <67215517+1411430556@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:59:18 +0800 Subject: [PATCH 383/506] docs: Correct the path to CONTRIBUTING.md links in the README files for each language to ensure that you point to the correct file location. (#12207) --- docs/README-DA.md | 2 +- docs/README-GR.md | 2 +- docs/README-JP.md | 2 +- docs/README-NO.md | 2 +- docs/README-TR.md | 2 +- docs/README-UA.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/README-DA.md b/docs/README-DA.md index d4eb0cab01b..2c6987053f3 100644 --- a/docs/README-DA.md +++ b/docs/README-DA.md @@ -15,7 +15,7 @@ Chat med os: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitte Endnu en fjernskrivebordssoftware, skrevet i Rust. Fungerer ud af æsken, ingen konfiguration påkrævet. Du har fuld kontrol over dine data uden bekymringer om sikkerhed. Du kan bruge vores rendezvous/relay-server, [opsætte din egen](https://rustdesk.com/server), eller [skrive din egen rendezvous/relay-server](https://github.com/rustdesk/rustdesk- server-demo). -RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for at få hjælp til at komme i gang. +RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) for at få hjælp til at komme i gang. [**PROGRAM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) diff --git a/docs/README-GR.md b/docs/README-GR.md index f21a05187b1..d834708ba6d 100644 --- a/docs/README-GR.md +++ b/docs/README-GR.md @@ -17,7 +17,7 @@ ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -Το RustDesk ενθαρρύνει τη συνεισφορά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε. +Το RustDesk ενθαρρύνει τη συνεισφορά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε. [**Συχνές ερωτήσεις**](https://github.com/rustdesk/rustdesk/wiki/FAQ) diff --git a/docs/README-JP.md b/docs/README-JP.md index d57dfd5105a..c49faee2448 100644 --- a/docs/README-JP.md +++ b/docs/README-JP.md @@ -18,7 +18,7 @@ Rustで書かれた、設定不要ですぐに使えるリモートデスクト ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) RustDeskは皆さんの貢献を歓迎します。 -貢献の方法については[CONTRIBUTING.md](docs/CONTRIBUTING.md)をご確認ください。 +貢献の方法については[CONTRIBUTING.md](CONTRIBUTING.md)をご確認ください。 [**よくある質問**](https://github.com/rustdesk/rustdesk/wiki/FAQ) diff --git a/docs/README-NO.md b/docs/README-NO.md index 4f18bb1c6b4..e77dcf8539b 100644 --- a/docs/README-NO.md +++ b/docs/README-NO.md @@ -17,7 +17,7 @@ Enda en annen fjernstyrt desktop programvare, skrevet i Rust. Virker rett ut av ![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. +RustDesk er velkommen for bidrag fra alle. Se [CONTRIBUTING.md](CONTRIBUTING-NO.md) for hjelp med oppstart. [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) diff --git a/docs/README-TR.md b/docs/README-TR.md index bdbfa9bd86b..d9481b2c745 100644 --- a/docs/README-TR.md +++ b/docs/README-TR.md @@ -18,7 +18,7 @@ Başka bir uzak masaüstü yazılımı daha, Rust dilinde yazılmış. Hemen kul ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs/CONTRIBUTING-TR.md) belgesine göz atın. +RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](CONTRIBUTING-TR.md) belgesine göz atın. [**SSS**](https://github.com/rustdesk/rustdesk/wiki/FAQ) diff --git a/docs/README-UA.md b/docs/README-UA.md index 5519e1b4494..fb880749423 100644 --- a/docs/README-UA.md +++ b/docs/README-UA.md @@ -17,7 +17,7 @@ ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk вітає внесок кожного. Ознайомтеся з [CONTRIBUTING.md](docs/CONTRIBUTING.md), щоб отримати допомогу на початковому етапі. +RustDesk вітає внесок кожного. Ознайомтеся з [CONTRIBUTING.md](CONTRIBUTING.md), щоб отримати допомогу на початковому етапі. [**ЧаПи**](https://github.com/rustdesk/rustdesk/wiki/FAQ) From 86e79b0162515be3a461dbeb9bdfb9ff860b5b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?COYG=E2=9A=A1=EF=B8=8F?= <67215517+1411430556@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:59:34 +0800 Subject: [PATCH 384/506] docs: correct jump to other language markdown files (#12205) --- docs/README-JP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README-JP.md b/docs/README-JP.md index c49faee2448..9beb259f2bb 100644 --- a/docs/README-JP.md +++ b/docs/README-JP.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]
    READMEやRustDesk UIRustDesk Docの翻訳者を歓迎します!

    From 7ad3023285bd2873f2ce61d7fd4e4b41cf932786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?COYG=E2=9A=A1=EF=B8=8F?= <67215517+1411430556@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:59:59 +0800 Subject: [PATCH 385/506] docs: Render correct "CAUTION" (#12204) --- docs/README-ZH.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README-ZH.md b/docs/README-ZH.md index fecfb4a4fb9..2899aa01bf7 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] | [Ελληνικά]

    -> [!警告] +> [!CAUTION] > **免责声明:**
    > RustDesk 的开发人员不纵容或支持任何不道德或非法的软件使用行为。滥用行为,例如未经授权的访问、控制或侵犯隐私,严格违反我们的准则。作者对应用程序的任何滥用行为概不负责。 From f766d28c3666cf37a80605b82c3252b8eb851b07 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 3 Jul 2025 17:27:50 +0800 Subject: [PATCH 386/506] Fix/linux keep terminal sessions (#12222) * fix: linux, keep terminal sessions Signed-off-by: fufesou * fix: terminal service stucked at reader join Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/ipc.rs | 19 +++++++++ src/platform/linux.rs | 6 +++ src/server/terminal_service.rs | 76 ++++++++++++++++++++++++++++------ 3 files changed, 88 insertions(+), 13 deletions(-) diff --git a/src/ipc.rs b/src/ipc.rs index 0c6a8d57e8c..1ae0481622f 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -282,6 +282,8 @@ pub enum Data { not(any(target_os = "android", target_os = "ios")) ))] ControllingSessionCount(usize), + #[cfg(target_os = "linux")] + TerminalSessionCount(usize), #[cfg(target_os = "windows")] PortForwardSessionCount(Option), SocksWs(Option, String)>>), @@ -642,6 +644,11 @@ async fn handle(data: Data, stream: &mut Connection) { Data::ControllingSessionCount(count) => { crate::updater::update_controlling_session_count(count); } + #[cfg(target_os = "linux")] + Data::TerminalSessionCount(_) => { + let count = crate::terminal_service::get_terminal_session_count(true); + allow_err!(stream.send(&Data::TerminalSessionCount(count)).await); + } #[cfg(feature = "hwcodec")] #[cfg(not(any(target_os = "android", target_os = "ios")))] Data::CheckHwcodec => { @@ -1388,6 +1395,18 @@ pub async fn update_controlling_session_count(count: usize) -> ResultType<()> { Ok(()) } +#[cfg(target_os = "linux")] +#[tokio::main(flavor = "current_thread")] +pub async fn get_terminal_session_count() -> ResultType { + let ms_timeout = 1_000; + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::TerminalSessionCount(0)).await?; + if let Some(Data::TerminalSessionCount(c)) = c.next_timeout(ms_timeout).await? { + return Ok(c); + } + Ok(0) +} + async fn handle_wayland_screencast_restore_token( key: String, value: String, diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 9fa69fa63ae..f17c5b472fa 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -370,6 +370,12 @@ fn should_start_server( && ((*cm0 && last_restart.elapsed().as_secs() > 60) || last_restart.elapsed().as_secs() > 3600) { + let terminal_session_count = crate::ipc::get_terminal_session_count().unwrap_or(0); + if terminal_session_count > 0 { + // There are terminal sessions, so we don't restart the server. + // We also need to keep `cm0` unchanged, so that we can reach this branch the next time. + return false; + } // restart server if new connections all closed, or every one hour, // as a workaround to resolve "SpotUdp" (dns resolve) // and x server get displays failure issue diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index 60accd720f8..d709454c941 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -8,6 +8,7 @@ use std::{ collections::{HashMap, VecDeque}, io::{Read, Write}, sync::{ + atomic::{AtomicBool, Ordering}, mpsc::{self, Receiver, SyncSender}, Arc, Mutex, }, @@ -141,11 +142,7 @@ fn remove_service(service_id: &str) { let sessions = service.lock().unwrap().sessions.clone(); for (_, session) in sessions.iter() { let mut session = session.lock().unwrap(); - if let Some(mut child) = session.child.take() { - // Kill the process - let _ = child.kill(); - add_to_reaper(child); - } + session.stop(); } } } @@ -265,6 +262,15 @@ fn ensure_cleanup_task() { } } +#[cfg(target_os = "linux")] +pub fn get_terminal_session_count(include_zombie_tasks: bool) -> usize { + let mut c = TERMINAL_SERVICES.lock().unwrap().len(); + if include_zombie_tasks { + c += TERMINAL_TASKS.lock().unwrap().len(); + } + c +} + pub fn new(service_id: String, is_persistent: bool) -> GenericService { // Create the service with initial persistence setting allow_err!(get_or_create_service(service_id.clone(), is_persistent)); @@ -393,6 +399,7 @@ pub struct TerminalSession { input_tx: Option>>, // Channel for receiving output from the reader thread output_rx: Option>>, + exiting: Arc, // Thread handles reader_thread: Option>, writer_thread: Option>, @@ -414,6 +421,7 @@ impl TerminalSession { child: None, input_tx: None, output_rx: None, + exiting: Arc::new(AtomicBool::new(false)), reader_thread: None, writer_thread: None, output_buffer: OutputBuffer::new(), @@ -428,29 +436,50 @@ impl TerminalSession { fn update_activity(&mut self) { self.last_activity = Instant::now(); } -} -impl Drop for TerminalSession { - fn drop(&mut self) { + // This helper function is to ensure that the threads are joined before the child process is dropped. + // Though this is not strictly necessary on macOS. + fn stop(&mut self) { + self.exiting.store(true, Ordering::SeqCst); + // Drop the input channel to signal writer thread to exit - drop(self.input_tx.take()); + if let Some(input_tx) = self.input_tx.take() { + // Send a final newline to ensure the reader can read some data, and then exit. + // This is required on Windows and Linux. + if let Err(e) = input_tx.send(b"\r\n".to_vec()) { + log::warn!("Failed to send final newline to the terminal: {}", e); + } + drop(input_tx); + } // Wait for threads to finish - if let Some(writer_thread) = self.writer_thread.take() { - let _ = writer_thread.join(); - } + // The reader thread should join before the writer thread on Windows. if let Some(reader_thread) = self.reader_thread.take() { let _ = reader_thread.join(); } - // Ensure child process is properly handled when session is dropped + // The read can read the last "\r\n" after the writer thread (not the child process) exits + // on Linux in my tests. + // But we still send "\r\n" to the writer thread and let the reader thread exit first for safety. + if let Some(writer_thread) = self.writer_thread.take() { + let _ = writer_thread.join(); + } + if let Some(mut child) = self.child.take() { + // Kill the process let _ = child.kill(); add_to_reaper(child); } } } +impl Drop for TerminalSession { + fn drop(&mut self) { + // Ensure child process is properly handled when session is dropped + self.stop(); + } +} + /// Persistent terminal service that can survive connection drops pub struct PersistentTerminalService { service_id: String, @@ -669,6 +698,17 @@ impl TerminalServiceProxy { let terminal_id = open.terminal_id; let writer_thread = thread::spawn(move || { let mut writer = writer; + // Write initial carriage return: + // 1. Windows requires at least one carriage return for `drop()` to work properly. + // Without this, the reader may fail to read the buffer after `input_tx.send(b"\r\n".to_vec()).ok();`. + // 2. This also refreshes the terminal interface on the controlling side (workaround for blank content on connect). + if let Err(e) = writer.write_all(b"\r") { + log::error!("Terminal {} initial write error: {}", terminal_id, e); + } else { + if let Err(e) = writer.flush() { + log::error!("Terminal {} initial flush error: {}", terminal_id, e); + } + } while let Ok(data) = input_rx.recv() { if let Err(e) = writer.write_all(&data) { log::error!("Terminal {} write error: {}", terminal_id, e); @@ -681,6 +721,7 @@ impl TerminalServiceProxy { log::debug!("Terminal {} writer thread exiting", terminal_id); }); + let exiting = session.exiting.clone(); // Spawn reader thread let terminal_id = open.terminal_id; let reader_thread = thread::spawn(move || { @@ -690,9 +731,14 @@ impl TerminalServiceProxy { match reader.read(&mut buf) { Ok(0) => { // EOF + // This branch can be reached when the child process exits on macOS. + // But not on Linux and Windows in my tests. break; } Ok(n) => { + if exiting.load(Ordering::SeqCst) { + break; + } let data = buf[..n].to_vec(); // Try to send, if channel is full, drop the data match output_tx.try_send(data) { @@ -710,6 +756,10 @@ impl TerminalServiceProxy { } } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // This branch is not reached in my tests, but we still add `exiting` check to ensure we can exit. + if exiting.load(Ordering::SeqCst) { + break; + } // For non-blocking I/O, sleep briefly thread::sleep(Duration::from_millis(10)); } From 9caf0dddc340aa9206054224483982ba7a52395f Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Fri, 4 Jul 2025 10:21:32 +0200 Subject: [PATCH 387/506] Update nl.rs (#12202) --- 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 ba068e712b8..0d4f6c808ca 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -699,9 +699,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable camera", "Camera inschakelen"), ("No cameras", "Geen camera's"), ("view_camera_unsupported_tip", "Het externe apparaat ondersteunt geen cameraweergave."), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), + ("Terminal", "Terminal"), + ("Enable terminal", "Terminal inschakelen"), + ("New tab", "Nieuw tabblad"), + ("Keep terminal sessions on disconnect", "Terminalsessies bij verbreking van de verbinding behouden"), ].iter().cloned().collect(); } From f3819e19d4b74b5ce1e78f0bf24f320b4ce4a3fa Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Fri, 4 Jul 2025 16:47:08 +0800 Subject: [PATCH 388/506] improve sas (#12226) * improve sas * Update windows.rs --- src/platform/windows.rs | 78 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 15661b35522..45c5fc7abfa 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -41,7 +41,9 @@ use winapi::{ um::{ errhandlingapi::GetLastError, handleapi::CloseHandle, - libloaderapi::{GetProcAddress, LoadLibraryA, LoadLibraryExA, LOAD_LIBRARY_SEARCH_SYSTEM32}, + libloaderapi::{ + GetProcAddress, LoadLibraryA, LoadLibraryExA, LOAD_LIBRARY_SEARCH_SYSTEM32, + }, minwinbase::STILL_ACTIVE, processthreadsapi::{ GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, @@ -784,7 +786,79 @@ pub fn send_sas() { } unsafe { log::info!("SAS received"); + + // Check and temporarily set SoftwareSASGeneration if needed + let mut original_value: Option = None; + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + if let Ok(policy_key) = hklm.open_subkey_with_flags( + "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", + KEY_READ | KEY_WRITE, + ) { + // Read current value + match policy_key.get_value::("SoftwareSASGeneration") { + Ok(value) => { + /* + - 0 = None (disabled) + - 1 = Services + - 2 = Ease of Access applications + - 3 = Services and Ease of Access applications (Both) + */ + if value != 1 && value != 3 { + original_value = Some(value); + log::info!("SoftwareSASGeneration is {}, setting to 1", value); + // Set to 1 for SendSAS to work + if let Err(e) = policy_key.set_value("SoftwareSASGeneration", &1u32) { + log::error!("Failed to set SoftwareSASGeneration: {}", e); + } + } + } + Err(e) => { + log::info!( + "SoftwareSASGeneration not found or error reading: {}, setting to 1", + e + ); + original_value = Some(0); // Mark that we need to restore (delete) it + // Create and set to 1 + if let Err(e) = policy_key.set_value("SoftwareSASGeneration", &1u32) { + log::error!("Failed to set SoftwareSASGeneration: {}", e); + } + } + } + } else { + log::error!("Failed to open registry key for SoftwareSASGeneration"); + } + + // Send SAS SendSAS(FALSE); + + // Restore original value if we changed it + if let Some(original) = original_value { + if let Ok(policy_key) = hklm.open_subkey_with_flags( + "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", + KEY_WRITE, + ) { + if original == 0 { + // It didn't exist before, delete it + if let Err(e) = policy_key.delete_value("SoftwareSASGeneration") { + log::error!("Failed to delete SoftwareSASGeneration: {}", e); + } else { + log::info!("Deleted SoftwareSASGeneration (restored to original state)"); + } + } else { + // Restore the original value + if let Err(e) = policy_key.set_value("SoftwareSASGeneration", &original) { + log::error!( + "Failed to restore SoftwareSASGeneration to {}: {}", + original, + e + ); + } else { + log::info!("Restored SoftwareSASGeneration to {}", original); + } + } + } + } } } @@ -1217,7 +1291,7 @@ fn get_after_install( // reg delete HKEY_CURRENT_USER\Software\Classes for // https://github.com/rustdesk/rustdesk/commit/f4bdfb6936ae4804fc8ab1cf560db192622ad01a // and https://github.com/leanflutter/uni_links_desktop/blob/1b72b0226cec9943ca8a84e244c149773f384e46/lib/src/protocol_registrar_impl_windows.dart#L30 - let hcu = winreg::RegKey::predef(HKEY_CURRENT_USER); + let hcu = RegKey::predef(HKEY_CURRENT_USER); hcu.delete_subkey_all(format!("Software\\Classes\\{}", exe)) .ok(); From 9389f3306de58b226df00e4a44148cc2259c3281 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 5 Jul 2025 09:24:06 +0800 Subject: [PATCH 389/506] fix https://github.com/rustdesk/rustdesk/issues/12233 --- src/rendezvous_mediator.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index d5c164800d8..9dd1695f107 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -122,6 +122,9 @@ impl RendezvousMediator { if elapsed < CONNECT_TIMEOUT { sleep(((CONNECT_TIMEOUT - elapsed) / 1000) as _).await; } + } else { + // https://github.com/rustdesk/rustdesk/issues/12233 + sleep(0.033).await; } } } From e2830347e66fac5c934175b19cba224c4ef40538 Mon Sep 17 00:00:00 2001 From: Lynilia <89228568+Lynilia@users.noreply.github.com> Date: Sun, 6 Jul 2025 11:59:51 +0200 Subject: [PATCH 390/506] Update fr.rs (#12203) --- src/lang/fr.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index d6a273cbe5b..5e1266fd83e 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -693,15 +693,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trackpad speed", "Vitesse du pavé tactile"), ("Default trackpad speed", "Vitesse par défaut du pavé tactile"), ("Numeric one-time password", "Mot de passe à usage unique numérique"), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("Enable IPv6 P2P connection", "Activer la connexion P2P IPv6"), + ("Enable UDP hole punching", "Activer le « hole punching » UDP"), ("View camera", "Afficher la caméra"), ("Enable camera", "Activer la caméra"), ("No cameras", "Aucune caméra"), ("view_camera_unsupported_tip", "L’appareil distant ne prend pas en charge l’affichage de la caméra."), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), + ("Terminal", "Terminal"), + ("Enable terminal", "Activer le terminal"), + ("New tab", "Nouvel onglet"), + ("Keep terminal sessions on disconnect", "Maintenir les sessions du terminal lors de la déconnexion"), ].iter().cloned().collect(); } From 7447a36782cdf9a2f42d37fc90ae98f5d3bcecae Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Sun, 6 Jul 2025 12:00:05 +0200 Subject: [PATCH 391/506] Italian language update (#12210) --- 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 4a1447f160a..96974e36cf3 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -699,9 +699,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable camera", "Abilita camera"), ("No cameras", "Nessuna camera"), ("view_camera_unsupported_tip", "Il dispositivo remoto non supporta la visualizzazione della camera."), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), + ("Terminal", "Terminale"), + ("Enable terminal", "Abilita terminale"), + ("New tab", "Nuova scheda"), + ("Keep terminal sessions on disconnect", "Quando disconetti mantieni attiva sessione terminale"), ].iter().cloned().collect(); } From dd7a124334cfe926b0ba15a368a2e0d01f55fe95 Mon Sep 17 00:00:00 2001 From: solokot Date: Sun, 6 Jul 2025 13:03:45 +0300 Subject: [PATCH 392/506] Update ru.rs (#12227) --- 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 e5d2e251817..0b40df0a7e4 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -699,9 +699,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable camera", "Включить камеру"), ("No cameras", "Камера отсутствует"), ("view_camera_unsupported_tip", "Удалённое устройство не поддерживает просмотр камеры."), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), + ("Terminal", "Терминал"), + ("Enable terminal", "Включить терминал"), + ("New tab", "Новая вкладка"), + ("Keep terminal sessions on disconnect", "Сохранять сеансы терминала при отключении"), ].iter().cloned().collect(); } From f15b9f05fbf2803d96ec09ae2f8501cd8eaadaa4 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:57:04 +0200 Subject: [PATCH 393/506] Update de.rs (#12215) --- 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 9b68d282170..54f32be6309 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -699,9 +699,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable camera", "Kamera zulassen"), ("No cameras", "Keine Kameras"), ("view_camera_unsupported_tip", "Das entfernte Gerät kann die Kamera nicht anzeigen."), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), + ("Terminal", "Terminal"), + ("Enable terminal", "Terminal zulassen"), + ("New tab", "Neuer Tab"), + ("Keep terminal sessions on disconnect", "Terminalsitzungen beim Trennen der Verbindung beibehalten"), ].iter().cloned().collect(); } From 0258b9adcab004397f2c9e3f57386f17ffb354ba Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Tue, 8 Jul 2025 10:19:49 +0200 Subject: [PATCH 394/506] Update nl.rs (#12216) From 94e76c3b6fa25e643f6cbd20c57c45d1311d2815 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:21:19 +0800 Subject: [PATCH 395/506] Git submodule: Bump libs/hbb_common from `f850a16` to `25e761f` (#12264) Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `f850a16` to `25e761f`. - [Release notes](https://github.com/rustdesk/hbb_common/releases) - [Commits](https://github.com/rustdesk/hbb_common/compare/f850a167ac403444451cf90c64d39fa6d3a58e1a...25e761f46778b567061770bc64d66332a4503332) --- updated-dependencies: - dependency-name: libs/hbb_common dependency-version: 25e761f46778b567061770bc64d66332a4503332 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index f850a167ac4..25e761f4677 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit f850a167ac403444451cf90c64d39fa6d3a58e1a +Subproject commit 25e761f46778b567061770bc64d66332a4503332 From aa680533ae58550a31ce99400cdb9fee72e3e599 Mon Sep 17 00:00:00 2001 From: John Fowler Date: Fri, 11 Jul 2025 16:32:14 +0200 Subject: [PATCH 396/506] Update hu.rs (#12267) Translate new strings. --- src/lang/hu.rs | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 18c12b17723..0849731c3b3 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -57,6 +57,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID Server", "ID kiszolgáló"), ("Relay Server", "Továbbító-kiszolgáló"), ("API Server", "API kiszolgáló"), + ("Key", "Kulcs"), ("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."), ("Invalid IP", "A megadott IP-cím érvénytelen"), ("Invalid format", "Érvénytelen formátum"), @@ -150,8 +151,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 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"), + ("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell biztosítania."), + ("config_screen", "Ahhoz, hogy távolról hozzáférhessen számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."), ("Installing ...", "Telepítés…"), ("Install", "Telepítés"), ("Installation", "Telepítés"), @@ -278,13 +279,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "Elfogadás?"), ("Open System Setting", "Rendszerbeállítások megnyitása"), ("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_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a \"Hozzáférhetőség\" szolgáltatás használatát."), ("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 kapcsolódási kérelmet küldhessenek"), + ("android_service_will_start_tip", "A képernyőmegosztás aktiválása automatikusan elindítja a szolgáltatást, így más eszközök is vezérelhetik ezt az Android-eszközt."), ("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 „továbbító-kiszolgáló-szolgáltatás indítása” gombra, vagy aktiválja a „Képernyőfelvétel” engedélyt."), + ("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \"Kapcsolási 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"), @@ -394,6 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via both", "Munkamenetek fogadása mindkettőn keresztül"), ("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ó"), + ("Numeric one-time password", "Numerikus, 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"), @@ -410,15 +412,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", "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", "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 az „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", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."), ("Elevate successfully", "Emelt szintű jogok megadva"), @@ -444,7 +446,7 @@ 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 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."), + ("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"), @@ -553,6 +555,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Keep terminal sessions on disconnect", "Terminál munkamenetek megtartása leválasztáskor"), ("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"), @@ -562,7 +565,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 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”."), + ("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"), @@ -618,6 +621,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Enable UDP hole punching", "UDP résszűrés engedélyezése"), + ("Enable IPv6 P2P connection", "IPv6 P2P kapcsolat engedélyezése"), ("Back", "Vissza"), ("Apps", "Alkalmazások"), ("Volume up", "Hangerő fel"), @@ -625,7 +630,7 @@ 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"), + ("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 perjel (\"/\") kezdetű, pl. \"/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"), @@ -646,7 +651,7 @@ 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 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."), + ("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"), @@ -655,7 +660,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Címkézetlen"), ("new-version-of-{}-tip", "A(z) {} új verziója"), ("Accessible devices", "Hozzáférhető eszközök"), + ("View camera", "Kamera nézet"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Frissítse a RustDesk klienst {} vagy újabb verziójára a távoli oldalon!"), + ("view_camera_unsupported_tip", "A kameranézet nem támogatott"), + ("Enable camera", "Kamera engedélyezése"), + ("Terminal", "Terminál"), + ("Enable terminal", "Terminál engedélyezése"), + ("No cameras", "Nincs kamera"), ("d3d_render_tip", "D3D renderelés"), ("Use D3D rendering", "D3D renderelés használata"), ("Printer", "Nyomtató"), @@ -692,16 +703,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use WebSocket", "WebSocket használata"), ("Trackpad speed", "Érintőpad sebessége"), ("Default trackpad speed", "Alapértelmezett érintőpad sebessége"), - ("Numeric one-time password", ""), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), - ("View camera", "Kamera megtekintése"), - ("Enable camera", "Kamera engedélyezése"), - ("No cameras", "Nincs kamera"), - ("view_camera_unsupported_tip", "A kameranézet nem támogatott"), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), + ("New tab", "Új lap"), ].iter().cloned().collect(); } From 0117e94e6ff565dedd083a06e1f9de173b41f87a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 11 Jul 2025 22:33:35 +0800 Subject: [PATCH 397/506] format --- src/lang/hu.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 0849731c3b3..df3044c6d37 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -57,7 +57,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID Server", "ID kiszolgáló"), ("Relay Server", "Továbbító-kiszolgáló"), ("API Server", "API kiszolgáló"), - ("Key", "Kulcs"), ("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."), ("Invalid IP", "A megadott IP-cím érvénytelen"), ("Invalid format", "Érvénytelen formátum"), @@ -395,7 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via both", "Munkamenetek fogadása mindkettőn keresztül"), ("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ó"), - ("Numeric one-time password", "Numerikus, 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"), @@ -555,7 +553,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), - ("Keep terminal sessions on disconnect", "Terminál munkamenetek megtartása leválasztáskor"), ("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"), @@ -621,8 +618,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), - ("Enable UDP hole punching", "UDP résszűrés engedélyezése"), - ("Enable IPv6 P2P connection", "IPv6 P2P kapcsolat engedélyezése"), ("Back", "Vissza"), ("Apps", "Alkalmazások"), ("Volume up", "Hangerő fel"), @@ -660,13 +655,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Untagged", "Címkézetlen"), ("new-version-of-{}-tip", "A(z) {} új verziója"), ("Accessible devices", "Hozzáférhető eszközök"), - ("View camera", "Kamera nézet"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Frissítse a RustDesk klienst {} vagy újabb verziójára a távoli oldalon!"), - ("view_camera_unsupported_tip", "A kameranézet nem támogatott"), - ("Enable camera", "Kamera engedélyezése"), - ("Terminal", "Terminál"), - ("Enable terminal", "Terminál engedélyezése"), - ("No cameras", "Nincs kamera"), ("d3d_render_tip", "D3D renderelés"), ("Use D3D rendering", "D3D renderelés használata"), ("Printer", "Nyomtató"), @@ -703,6 +692,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use WebSocket", "WebSocket használata"), ("Trackpad speed", "Érintőpad sebessége"), ("Default trackpad speed", "Alapértelmezett érintőpad sebessége"), - ("New tab", "Új lap"), + ("Numeric one-time password", "Numerikus, egyszer használatos jelszó"), + ("Enable IPv6 P2P connection", "IPv6 P2P kapcsolat engedélyezése"), + ("Enable UDP hole punching", "UDP résszűrés engedélyezése"), + ("View camera", "Kamera nézet"), + ("Enable camera", "Kamera engedélyezése"), + ("No cameras", "Nincs kamera"), + ("view_camera_unsupported_tip", "A kameranézet nem támogatott"), + ("Terminal", "Terminál"), + ("Enable terminal", "Terminál engedélyezése"), + ("New tab", "Új lap"), + ("Keep terminal sessions on disconnect", "Terminál munkamenetek megtartása leválasztáskor"), ].iter().cloned().collect(); } From 331b624cd627a0d2dd17a3486c41054395a7ea70 Mon Sep 17 00:00:00 2001 From: Kleofass <4000163+Kleofass@users.noreply.github.com> Date: Sat, 12 Jul 2025 08:40:45 +0300 Subject: [PATCH 398/506] Update lv.rs (#12270) --- src/lang/lv.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 327a4531731..9ef5c38f04e 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -693,15 +693,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Trackpad speed", "Skārienpaliktņa ātrums"), ("Default trackpad speed", "Noklusējuma skārienpaliktņa ātrums"), ("Numeric one-time password", "Vienreiz lietojama ciparu parole"), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("Enable IPv6 P2P connection", "Iespējot IPv6 P2P savienojumu"), + ("Enable UDP hole punching", "Iespējot UDP caurumu veidošanu"), ("View camera", "Skatīt kameru"), ("Enable camera", "Iespējot kameru"), ("No cameras", "Nav kameru"), ("view_camera_unsupported_tip", "Attālā ierīce neatbalsta kameras skatīšanos."), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), + ("Terminal", "Terminālis"), + ("Enable terminal", "Iespējot termināli"), + ("New tab", "Jauna cilne"), + ("Keep terminal sessions on disconnect", "Atvienojoties saglabāt termināļa sesijas"), ].iter().cloned().collect(); } From 856362006a339705d7fbab4f064f3db35eaa7464 Mon Sep 17 00:00:00 2001 From: LittleFishYu2008 <99793130+LittleFishYu2008@users.noreply.github.com> Date: Sun, 13 Jul 2025 16:08:41 +0800 Subject: [PATCH 399/506] Update cn.rs (#12281) * Update cn.rs * Update cn.rs * Update cn.rs * Update cn.rs --- src/lang/cn.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index f7110573f42..18915464b2a 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -686,22 +686,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("{} Update", "{} 更新"), ("{}-to-update-tip", "即将关闭 {} ,并安装新版本。"), ("download-new-version-failed-tip", "下载失败,您可以重试或者点击\"下载\"按钮,从发布网址下载,并手动升级。"), - ("Auto update", ""), + ("Auto update", "自动更新"), ("update-failed-check-msi-tip", "安装方式检测失败。请点击\"下载\"按钮,从发布网址下载,并手动升级。"), ("websocket_tip", "使用 WebSocket 时,仅支持中继连接。"), ("Use WebSocket", "使用 WebSocket"), ("Trackpad speed", "触控板速度"), ("Default trackpad speed", "默认触控板速度"), ("Numeric one-time password", "一次性密码为数字"), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("Enable IPv6 P2P connection", "启用 IPv6 P2P 连接"), + ("Enable UDP hole punching", "启用 UDP 打洞"), ("View camera", "查看摄像头"), ("Enable camera", "允许查看摄像头"), ("No cameras", "没有摄像头"), ("view_camera_unsupported_tip", "您的远程端不支持查看摄像头。"), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), + ("Terminal", "终端"), + ("Enable terminal", "启用终端"), + ("New tab", "新建选项卡"), + ("Keep terminal sessions on disconnect", "断开连接时保持终端会话"), ].iter().cloned().collect(); } From ae255c83ee30d78bd9eb0635f080602c41dee840 Mon Sep 17 00:00:00 2001 From: Mahdi Rahimi <31624047+mahdirahimi1999@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:58:01 +0330 Subject: [PATCH 400/506] Updated Persian translations in fa.rs (#12283) --- src/lang/fa.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 538e0610c07..1836c2742e0 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -699,9 +699,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable camera", "فعال کردن دوربین"), ("No cameras", "هیچ دوربینی یافت نشد"), ("view_camera_unsupported_tip", "دوربین در این دستگاه پشتیبانی نمی‌شود"), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), + ("Terminal", "ترمینال"), + ("Enable terminal", "فعال‌سازی ترمینال"), + ("New tab", "زبانه جدید"), + ("Keep terminal sessions on disconnect", "حفظ جلسات ترمینال پس از قطع اتصال"), ].iter().cloned().collect(); } From 8c68b8326593a6722e85a63054816b2636f12c78 Mon Sep 17 00:00:00 2001 From: Mahdi Rahimi <31624047+mahdirahimi1999@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:12:07 +0330 Subject: [PATCH 401/506] Update Arabic translation in ar.rs (#12284) --- src/lang/ar.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 8da03788f38..38e2123776b 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -699,9 +699,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable camera", "تمكين الكاميرا"), ("No cameras", "لا توجد كاميرات"), ("view_camera_unsupported_tip", "عرض الكاميرا غير مدعوم في هذا الجهاز"), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), + ("Terminal", "الطرفية"), + ("Enable terminal", "تمكين الطرفية"), + ("New tab", "تبويب جديد"), + ("Keep terminal sessions on disconnect", "الاحتفاظ بجلسات الطرفية عند قطع الاتصال"), ].iter().cloned().collect(); } From 8d559725d5789e693fb1c3d1ce2100a10d9f015e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Tue, 15 Jul 2025 16:42:19 +0900 Subject: [PATCH 402/506] Update ko.rs (#12298) * Update ko.rs * Update ko.rs * Update ko.rs --- src/lang/ko.rs | 704 ++++++++++++++++++++++++------------------------- 1 file changed, 352 insertions(+), 352 deletions(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 863748e3763..0efc42bdb6c 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -3,16 +3,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "상태"), ("Your Desktop", "내 데스크탑"), - ("desk_tip", "이 ID와 비밀번호를 사용하여 원격으로 이 데스크톱에 액세스할 수 있습니다."), + ("desk_tip", "이 ID와 비밀번호로 데스크톱에 액세스할 수 있습니다."), ("Password", "비밀번호"), - ("Ready", "준비됨"), + ("Ready", "준비"), ("Established", "연결됨"), - ("connecting_status", "RustDesk 네트워크에 연결하는 중..."), + ("connecting_status", "RustDesk 네트워크에 연결 중..."), ("Enable service", "서비스 활성화"), ("Start service", "서비스 시작"), - ("Service is running", "서비스 실행 중"), - ("Service is not running", "서비스 중지됨"), - ("not_ready_status", "준비되지 않았습니다. 네트워크 연결을 확인해 주세요."), + ("Service is running", "서비스가 실행 중 입니다"), + ("Service is not running", "서비스가 실행되지 않았습니다"), + ("not_ready_status", "준비되지 않았습니다. 연결을 확인해 주세요"), ("Control Remote Desktop", "원격 데스크탑 제어"), ("Transfer file", "파일 전송"), ("Connect", "연결"), @@ -21,31 +21,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Confirmation", "확인"), ("TCP tunneling", "TCP 터널링"), ("Remove", "삭제"), - ("Refresh random password", "랜덤 비밀번호 갱신"), - ("Set your own password", "사용자 지정 비밀번호 설정"), - ("Enable keyboard/mouse", "키보드/마우스 활성화"), - ("Enable clipboard", "클립보드 활성화"), - ("Enable file transfer", "파일 전송 활성화"), - ("Enable TCP tunneling", "TCP 터널링 활성화"), + ("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", "잘못된 서버 구성입니다."), + ("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"), - ("length %min% to %max%", "길이: %min% ~ %max%"), + ("length %min% to %max%", "길이 %min% ~ %max%"), ("starts with a letter", "문자로 시작해야 합니다"), ("allowed characters", "허용되는 문자"), - ("id_change_tip", "ID는 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", "정보"), ("Slogan_tip", "이 혼란스러운 세상에서 마음을 담아 만들었습니다!"), - ("Privacy Statement", "개인정보처리방침"), + ("Privacy Statement", "개인정보 보호정책"), ("Mute", "음소거"), ("Build Date", "빌드 날짜"), ("Version", "버전"), @@ -53,43 +53,43 @@ 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", "릴레이 서버"), ("API Server", "API 서버"), ("invalid_http", "http:// 또는 https://로 시작해야 합니다"), - ("Invalid IP", "유효하지 않은 IP 주소입니다."), - ("Invalid format", "유효하지 않은 형식입니다."), - ("server_not_support", "서버에서 아직 지원하지 않는 기능입니다."), - ("Not available", "사용할 수 없습니다."), - ("Too frequent", "변경 요청이 너무 잦습니다. 잠시 후 다시 시도해 주세요."), + ("Invalid IP", "유효하지 않은 IP 주소입니다"), + ("Invalid format", "유효하지 않은 형식입니다"), + ("server_not_support", "아직 서버에서 지원되지 않습니다"), + ("Not available", "사용할 수 없음"), + ("Too frequent", "너무 빈번합니다"), ("Cancel", "취소"), ("Skip", "건너뛰기"), ("Close", "닫기"), ("Retry", "재시도"), ("OK", "확인"), - ("Password Required", "비밀번호가 필요합니다."), - ("Please enter your password", "비밀번호를 입력해 주세요."), + ("Password Required", "비밀번호 필요"), + ("Please enter your password", "비밀번호를 입력하세요"), ("Remember password", "비밀번호 기억"), - ("Wrong Password", "잘못된 비밀번호입니다."), + ("Wrong Password", "잘못된 비밀번호"), ("Do you want to enter again?", "다시 입력하시겠습니까?"), ("Connection Error", "연결 오류"), ("Error", "오류"), - ("Reset by the peer", "피어에 의해 연결이 초기화되었습니다."), - ("Connecting...", "연결 중입니다..."), - ("Connection in progress. Please wait.", "연결 진행 중입니다. 잠시만 기다려 주세요."), - ("Please try 1 minute later", "1분 후에 다시 시도해 주세요."), + ("Reset by the peer", "피어에 의해 초기화"), + ("Connecting...", "연결 중..."), + ("Connection in progress. Please wait.", "연결이 진행 중입니다. 잠시만 기다려 주세요."), + ("Please try 1 minute later", "1분 후에 다시 시도하세요"), ("Login Error", "로그인 오류"), - ("Successful", "성공했습니다."), - ("Connected, waiting for image...", "연결되었습니다. 화면을 기다리는 중입니다..."), + ("Successful", "성공"), + ("Connected, waiting for image...", "연결되었습니다, 이미지를 기다리는 중..."), ("Name", "이름"), ("Type", "유형"), - ("Modified", "수정일"), + ("Modified", "수정 날짜"), ("Size", "크기"), - ("Show Hidden Files", "숨겨진 파일 표시"), + ("Show Hidden Files", "숨김 파일 표시"), ("Receive", "받기"), ("Send", "보내기"), - ("Refresh File", "파일 새로고침"), + ("Refresh File", "파일 새로 고침"), ("Local", "로컬"), ("Remote", "원격"), ("Remote Computer", "원격 컴퓨터"), @@ -98,29 +98,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "삭제"), ("Properties", "속성"), ("Multi Select", "다중 선택"), - ("Select All", "전체 선택"), - ("Unselect All", "전체 선택 해제"), - ("Empty Directory", "빈 폴더입니다"), - ("Not an empty directory", "폴더가 비어 있지 않습니다"), + ("Select All", "모두 선택"), + ("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", "완료되었습니다."), + ("Finished", "완료되었습니다"), ("Speed", "속도"), ("Custom Image Quality", "사용자 지정 이미지 품질"), - ("Privacy mode", "프라이버시 모드"), + ("Privacy mode", "개인정보 보호 모드"), ("Block user input", "사용자 입력 차단"), ("Unblock user input", "사용자 입력 차단 해제"), ("Adjust Window", "창 크기 조정"), - ("Original", "원본 크기"), + ("Original", "원본"), ("Shrink", "축소"), - ("Stretch", "확대"), - ("Scrollbar", "스크롤바"), + ("Stretch", "늘이기"), + ("Scrollbar", "스크롤 막대"), ("ScrollAuto", "자동 스크롤"), ("Good image quality", "좋은 이미지 품질"), ("Balanced", "균형 잡힌"), @@ -128,203 +128,203 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", "사용자 지정"), ("Show remote cursor", "원격 커서 표시"), ("Show quality monitor", "품질 모니터 표시"), - ("Disable clipboard", "클립보드 비활성화"), - ("Lock after session end", "세션 종료 후 화면 잠금"), - ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del 입력"), - ("Insert Lock", "입력 잠금"), + ("Disable clipboard", "클립보드 사용 안 함"), + ("Lock after session end", "세션 종료 후 잠금"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del 삽입"), + ("Insert Lock", "삽입 잠금"), ("Refresh", "새로 고침"), - ("ID does not exist", "ID가 존재하지 않습니다."), - ("Failed to connect to rendezvous server", "랑데부 서버 연결에 실패했습니다."), - ("Please try later", "나중에 다시 시도해 주세요."), - ("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", "원격 데스크톱에 직접 연결하지 못했습니다."), + ("ID does not exist", "ID가 존재하지 않습니다"), + ("Failed to connect to rendezvous server", "랑데부 서버 연결에 실패했습니다"), + ("Please try later", "나중에 시도해 주세요"), + ("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", "비밀번호 설정"), ("OS Password", "OS 비밀번호"), - ("install_tip", "UAC(사용자 계정 컨트롤)로 인해 일부 원격 제어 기능이 제한될 수 있습니다. 모든 기능을 사용하려면 RustDesk를 시스템에 설치해야 합니다."), - ("Click to upgrade", "업그레이드하려면 클릭하세요."), - ("Click to download", "다운로드하려면 클릭하세요."), - ("Click to update", "업데이트하려면 클릭하세요."), + ("install_tip", "UAC로 인해 경우에 따라 RustDesk가 원격 쪽에서 제대로 작동하지 않을 수 있습니다. UAC를 피하려면 아래 버튼을 클릭하여 시스템에 RustDesk를 설치하세요."), + ("Click to upgrade", "업그레이드하려면 클릭"), + ("Click to download", "다운로드하려면 클릭"), + ("Click to update", "업데이트하려면 클릭"), ("Configure", "구성"), - ("config_acc", "이 데스크톱을 원격으로 제어하려면 RustDesk에 '손쉬운 사용' 권한을 부여해야 합니다."), - ("config_screen", "이 데스크톱 화면을 원격으로 보려면 RustDesk에 '화면 기록' 권한을 부여해야 합니다."), - ("Installing ...", "설치 중입니다..."), + ("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 ...", "생성 중..."), - ("Your installation is lower version.", "현재 설치된 버전이 실행 중인 버전보다 낮습니다."), - ("not_close_tcp_tip", "TCP 터널링 연결 중에는 이 창을 닫지 마십시오."), - ("Listening ...", "연결 대기 중..."), + ("Create start menu shortcuts", "시작 메뉴에 바로가기 만들기"), + ("Create desktop icon", "바탕 화면 아이콘 만들기"), + ("agreement_tip", "설치를 시작하면 라이선스 계약을 수락하는 것입니다."), + ("Accept and Install", "수락하고 설치"), + ("End-user license agreement", "최종 사용자 라이선스 약관 동의"), + ("Generating ...", "생성 중 ..."), + ("Your installation is lower version.", "설치 버전이 하위 버전입니다."), + ("not_close_tcp_tip", "터널을 사용하는 동안에는 이 창을 닫지 마세요"), + ("Listening ...", "청취 중 ..."), ("Remote Host", "원격 호스트"), ("Remote Port", "원격 포트"), - ("Action", "액션"), + ("Action", "동작"), ("Add", "추가"), ("Local Port", "로컬 포트"), ("Local Address", "로컬 주소"), ("Change Local Port", "로컬 포트 변경"), - ("setup_server_tip", "자체 서버를 설정하면 더 빠른 연결 속도를 경험할 수 있습니다."), - ("Too short, at least 6 characters.", "너무 짧습니다. 최소 6자 이상 입력해 주세요."), - ("The confirmation is not identical.", "확인 입력이 일치하지 않습니다."), + ("setup_server_tip", "더 빠른 연결을 위해, 자신만의 서버를 설정해 주세요."), + ("Too short, at least 6 characters.", "너무 짧습니다. 최소 6자 이상입니다."), + ("The confirmation is not identical.", "확인이 동일하지 않습니다."), ("Permissions", "권한"), ("Accept", "수락"), - ("Dismiss", "무시"), - ("Disconnect", "연결 종료"), - ("Enable file copy and paste", "파일 복사/붙여넣기 허용"), + ("Dismiss", "거부"), + ("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", "비밀번호를 입력하세요"), + ("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 세션 공유 활성화"), + ("Enable RDP session sharing", "RDP 세션 공유 사용함"), ("Auto Login", "자동 로그인"), - ("Enable direct IP access", "직접 IP 접속 활성화"), - ("Rename", "이름 변경"), - ("Space", "공간"), - ("Create desktop shortcut", "데스크탑 바로가기 생성"), + ("Enable direct IP access", "직접 IP 액세스 사용함"), + ("Rename", "이름 바꾸기"), + ("Space", "공백"), + ("Create desktop shortcut", "바탕 화면 바로가기 만들기"), ("Change Path", "경로 변경"), - ("Create Folder", "폴더 생성"), - ("Please enter the folder name", "폴더 이름을 입력해 주세요."), + ("Create Folder", "폴더 만들기"), + ("Please enter the folder name", "폴더 이름을 입력해주세요"), ("Fix it", "문제 해결"), ("Warning", "경고"), - ("Login screen using Wayland is not supported", "Wayland를 사용한 로그인 화면은 지원되지 않습니다."), - ("Reboot required", "재부팅이 필요합니다."), - ("Unsupported display server", "지원하지 않는 디스플레이 서버입니다."), - ("x11 expected", "X11 환경이 필요합니다."), + ("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", "상대방이 수동으로 연결을 종료했습니다."), - ("Enable remote configuration modification", "원격 설정 변경 허용"), - ("Run without install", "설치하지 않고 실행"), - ("Connect via relay", "릴레이 서버를 통해 연결"), - ("Always connect via relay", "항상 릴레이 서버를 통해 연결"), - ("whitelist_tip", "IP 화이트리스트에 등록된 IP 주소만 이 기기에 연결할 수 있습니다."), + ("Username", "사용자 이름"), + ("Invalid port", "유효하지 않은 포트입니다"), + ("Closed manually by the peer", "피어가 수동으로 닫았습니다"), + ("Enable remote configuration modification", "원격 구성 수정 사용함"), + ("Run without install", "설치 없이 실행"), + ("Connect via relay", "릴레이를 통해 연결"), + ("Always connect via relay", "항상 릴레이를 통해 연결"), + ("whitelist_tip", "화이트리스트에 있는 IP만 나에게 액세스할 수 있음"), ("Login", "로그인"), - ("Verify", "인증"), - ("Remember me", "로그인 정보 기억"), - ("Trust this device", "이 기기 신뢰"), - ("Verification code", "인증 번호"), - ("verification_tip", "등록된 이메일 주소로 인증 코드를 보냈습니다. 코드를 입력하여 로그인을 완료하세요."), + ("Verify", "확인"), + ("Remember me", "기억하기"), + ("Trust this device", "이 장치 신뢰"), + ("Verification code", "인증 코드"), + ("verification_tip", "등록한 이메일 주소로 인증 코드가 전송되었으니 인증 코드를 입력하여 로그인을 계속하세요."), ("Logout", "로그아웃"), ("Tags", "태그"), ("Search ID", "ID 검색"), - ("whitelist_sep", "쉼표(,), 세미콜론(;), 공백 또는 줄바꿈으로 구분하여 여러 IP를 입력할 수 있습니다."), + ("whitelist_sep", "쉼표, 세미콜론, 공백 또는 새 줄로 구분합니다."), ("Add ID", "ID 추가"), ("Add Tag", "태그 추가"), ("Unselect all tags", "모든 태그 선택 해제"), ("Network error", "네트워크 오류"), - ("Username missed", "사용자 이름을 입력해 주세요."), - ("Password missed", "비밀번호를 입력해 주세요."), - ("Wrong credentials", "로그인 정보가 정확하지 않습니다."), - ("The verification code is incorrect or has expired", "인증 코드가 정확하지 않거나 만료되었습니다."), - ("Edit Tag", "태그 수정"), - ("Forget Password", "비밀번호 기억하지 않기"), + ("Username missed", "사용자 이름이 누락되었습니다"), + ("Password missed", "비밀번호가 누락되었습니다"), + ("Wrong credentials", "잘못된 자격 증명"), + ("The verification code is incorrect or has expired", "인증 코드가 올바르지 않거나 만료되었습니다."), + ("Edit Tag", "태그 편집"), + ("Forget Password", "비밀번호 분실"), ("Favorites", "즐겨찾기"), ("Add to Favorites", "즐겨찾기에 추가"), ("Remove from Favorites", "즐겨찾기에서 삭제"), - ("Empty", "비어있음"), - ("Invalid folder name", "유효하지 않은 폴더명"), + ("Empty", "비어 있음"), + ("Invalid folder name", "유효하지 않은 폴더 이름"), ("Socks5 Proxy", "Socks5 프록시"), - ("Socks5/Http(s) Proxy", "Socks5/HTTP(S) 프록시 서버"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) 프록시"), ("Discovered", "발견됨"), - ("install_daemon_tip", "부팅 시 자동으로 시작하려면 시스템 서비스를 설치해야 합니다."), + ("install_daemon_tip", "부팅할 때 시작하려면 시스템 서비스를 설치해야 합니다."), ("Remote ID", "원격 ID"), ("Paste", "붙여넣기"), - ("Paste here?", "여기에 붙여넣을까요?"), - ("Are you sure to close the connection?", "연결을 종료할까요?"), + ("Paste here?", "여기에 붙여넣으시겠습니까?"), + ("Are you sure to close the connection?", "연결을 종료하시겠습니까?"), ("Download new version", "새 버전 다운로드"), ("Touch mode", "터치 모드"), ("Mouse mode", "마우스 모드"), ("One-Finger Tap", "한 손가락 탭"), ("Left Mouse", "왼쪽 마우스"), - ("One-Long Tap", "길게 탭하기"), + ("One-Long Tap", "한 번 길게 탭"), ("Two-Finger Tap", "두 손가락 탭"), ("Right Mouse", "오른쪽 마우스"), ("One-Finger Move", "한 손가락으로 이동"), - ("Double Tap & Move", "두 번 탭 후 이동"), - ("Mouse Drag", "마우스 드래그"), - ("Three-Finger vertically", "세 손가락으로 수직 스크롤"), + ("Double Tap & Move", "두 번 탭하고 이동"), + ("Mouse Drag", "마우스 끌기"), + ("Three-Finger vertically", "세 손가락으로 수직"), ("Mouse Wheel", "마우스 휠"), ("Two-Finger Move", "두 손가락으로 이동"), ("Canvas Move", "캔버스 이동"), - ("Pinch to Zoom", "손가락으로 확대/축소"), - ("Canvas Zoom", "캔버스 확대"), + ("Pinch to Zoom", "찝어서 확대/축소"), + ("Canvas Zoom", "캔버스 확대/축소"), ("Reset canvas", "캔버스 초기화"), - ("No permission of file transfer", "파일 전송 권한이 없습니다."), - ("Note", "메모"), + ("No permission of file transfer", "파일 전송 권한이 없습니다"), + ("Note", "노트"), ("Connection", "연결"), ("Share screen", "화면 공유"), ("Chat", "채팅"), - ("Total", "총"), - ("items", "개"), + ("Total", "전체"), + ("items", "항목"), ("Selected", "선택됨"), ("Screen Capture", "화면 캡처"), ("Input Control", "입력 제어"), ("Audio Capture", "오디오 캡처"), ("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_new_connection_tip", "새로운 원격 제어 요청이 있습니다."), - ("android_service_will_start_tip", "'화면 공유'를 켜면 서비스가 자동으로 시작되어 다른 기기에서 이 기기로 연결을 요청할 수 있습니다."), - ("android_stop_service_tip", "서비스를 중지하면 현재 활성화된 모든 연결이 끊어집니다."), - ("android_version_audio_tip", "현재 Android 버전은 오디오 공유를 지원하지 않습니다. Android 10 이상으로 업그레이드하세요."), - ("android_start_service_tip", "'서비스 시작'을 탭하거나 '화면 공유' 권한을 켜서 화면 공유 서비스를 시작하세요."), - ("android_permission_may_not_change_tip", "이미 연결된 세션의 권한은 다시 연결하기 전까지 적용되지 않을 수 있습니다."), + ("How to get Android input permission?", "Android 입력 권한을 얻는 방법은?"), + ("android_input_permission_tip1", "원격 장치에서 마우스나 터치로 Android 장치를 제어하려면 RustDesk가 \"접근성\" 서비스를 사용하도록 허용해야 합니다."), + ("android_input_permission_tip2", "다음 시스템 설정 페이지로 이동하여 [설치된 서비스]를 찾아 들어가서 [RustDesk 입력] 서비스를 켜세요."), + ("android_new_connection_tip", "현재 장치를 제어하려는 새로운 제어 요청이 수신되었습니다."), + ("android_service_will_start_tip", "\"화면 캡처\"를 켜면 자동으로 서비스가 시작되어 다른 장치가 내 장치에 연결을 요청할 수 있습니다."), + ("android_stop_service_tip", "서비스를 닫으면 설정된 모든 연결이 자동으로 닫힙니다."), + ("android_version_audio_tip", "현재 Android 버전은 오디오 캡처를 지원하지 않으므로 Android 10 이상으로 업그레이드하세요."), + ("android_start_service_tip", "[서비스 시작]을 탭하거나 [화면 캡처] 권한을 활성화하여 화면 공유 서비스를 시작합니다."), + ("android_permission_may_not_change_tip", "설정된 연결에 대한 권한은 다시 연결할 때까지 즉시 변경되지 않을 수 있습니다."), ("Account", "계정"), ("Overwrite", "덮어쓰기"), - ("This file exists, skip or overwrite this file?", "같은 이름의 파일이 이미 존재합니다. 건너뛰거나 덮어쓰시겠습니까?"), + ("This file exists, skip or overwrite this file?", "이 파일이 이미 존재합니다, 건너뛰거나 덮어쓰시겠습니까?"), ("Quit", "종료"), ("Help", "도움말"), ("Failed", "실패"), ("Succeeded", "성공"), - ("Someone turns on privacy mode, exit", "프라이버시 모드가 활성화되어 연결이 종료됩니다."), + ("Someone turns on privacy mode, exit", "누군가 개인정보 보호 모드를 켜고 종료합니다"), ("Unsupported", "지원되지 않음"), - ("Peer denied", "상대방이 연결 요청을 거부했습니다."), - ("Please install plugins", "플러그인을 설치하세요."), - ("Peer exit", "상대방이 연결을 종료했습니다."), + ("Peer denied", "연결 거부됨"), + ("Please install plugins", "플러그인을 설치해주세요"), + ("Peer exit", "피어 종료"), ("Failed to turn off", "끄기 실패"), ("Turned off", "꺼짐"), ("Language", "언어"), - ("Keep RustDesk background service", "RustDesk 백그라운드 서비스 실행 유지"), - ("Ignore Battery Optimizations", "배터리 최적화에서 제외"), - ("android_open_battery_optimizations_tip", "배터리 최적화 대상에서 제외하려면, RustDesk 앱 설정 페이지로 이동하여 [배터리] 항목에서 [제한 없음]을 선택하세요."), - ("Start on boot", "부팅 시 자동 시작"), - ("Start the screen sharing service on boot, requires special permissions", "부팅 시 화면 공유 서비스를 시작하려면 특별한 권한이 필요합니다."), - ("Connection not allowed", "연결이 허용되지 않았습니다."), + ("Keep RustDesk background service", "RustDesk 백그라운드 서비스 유지"), + ("Ignore Battery Optimizations", "배터리 최적화 무시"), + ("android_open_battery_optimizations_tip", "이 기능을 비활성화하려면 다음 RustDesk 응용 프로그램 설정 페이지로 이동하여 [배터리]를 찾아서 입력하고 [제한 없음]을 선택 취소하세요"), + ("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", "(일회용/영구) 비밀번호 모두 사용"), + ("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", "복사되었습니다."), + ("Enable remote restart", "원격 재시작 사용함"), + ("Restart remote device", "원격 장치 다시 시작"), + ("Are you sure you want to restart", "다시 시작하시겠습니까"), + ("Restarting remote device", "원격 장치를 다시 시작하는 중"), + ("remote_restarting_tip", "원격 장치가 다시 시작되고 있습니다. 이 메시지 상자를 닫고 잠시 후 영구 비밀번호로 다시 연결해 주세요"), + ("Copied", "복사되었습니다"), ("Exit Fullscreen", "전체 화면 종료"), ("Fullscreen", "전체 화면"), ("Mobile Actions", "모바일 작업"), ("Select Monitor", "모니터 선택"), ("Control Actions", "제어 작업"), - ("Display Settings", "화면 설정"), + ("Display Settings", "디스플레이 설정"), ("Ratio", "비율"), ("Image Quality", "이미지 품질"), ("Scroll Style", "스크롤 스타일"), @@ -334,25 +334,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "릴레이 연결"), ("Secure Connection", "보안 연결"), ("Insecure Connection", "보안되지 않은 연결"), - ("Scale original", "원본 크기로 조정"), - ("Scale adaptive", "창에 맞게 조정"), + ("Scale original", "원본 크기 조정"), + ("Scale adaptive", "크기 조정 가능"), ("General", "일반"), ("Security", "보안"), ("Theme", "테마"), ("Dark Theme", "어두운 테마"), ("Light Theme", "밝은 테마"), - ("Dark", "어둡게"), - ("Light", "밝게"), + ("Dark", "어두운"), + ("Light", "밝은"), ("Follow System", "시스템 설정 따름"), ("Enable hardware codec", "하드웨어 코덱 활성화"), ("Unlock Security Settings", "보안 설정 잠금 해제"), - ("Enable audio", "오디오 활성화"), + ("Enable audio", "오디오 사용함"), ("Unlock Network Settings", "네트워크 설정 잠금 해제"), ("Server", "서버"), - ("Direct IP Access", "IP 주소로 직접 연결"), + ("Direct IP Access", "직접 IP 연결"), ("Proxy", "프록시"), ("Apply", "적용"), - ("Disconnect all devices?", "모든 기기의 연결을 해제하시겠습니까?"), + ("Disconnect all devices?", "모든 장치의 연결을 해제하시겠습니까?"), ("Clear", "지우기"), ("Audio Input Device", "오디오 입력 장치"), ("Use IP Whitelisting", "IP 화이트리스트 사용"), @@ -360,68 +360,68 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Pin Toolbar", "도구 모음 고정"), ("Unpin Toolbar", "도구 모음 고정 해제"), ("Recording", "녹화"), - ("Directory", "저장 위치"), - ("Automatically record incoming sessions", "수신 세션을 자동으로 녹화"), - ("Automatically record outgoing sessions", "발신 세션을 자동으로 녹화"), + ("Directory", "디렉터리"), + ("Automatically record incoming sessions", "들어오는 세션 자동 녹화"), + ("Automatically record outgoing sessions", "나가는 세션 자동 녹화"), ("Change", "변경"), ("Start session recording", "세션 녹화 시작"), ("Stop session recording", "세션 녹화 중지"), - ("Enable recording session", "세션 녹화 활성화"), - ("Enable LAN discovery", "LAN 검색 허용"), + ("Enable recording session", "세션 녹화 사용함"), + ("Enable LAN discovery", "LAN 검색 사용함"), ("Deny LAN discovery", "LAN 검색 거부"), - ("Write a message", "메시지 작성"), + ("Write a message", "메시지 쓰기"), ("Prompt", "프롬프트"), - ("Please wait for confirmation of UAC...", "상대방의 UAC(사용자 계정 컨트롤) 확인을 기다리는 중입니다..."), - ("elevated_foreground_window_tip", "원격 데스크톱의 현재 창을 제어하려면 관리자 권한이 필요합니다. 일시적으로 마우스와 키보드를 사용할 수 없다면, 상대방에게 현재 창을 최소화하도록 요청하거나 연결 관리 창에서 '권한 상승'을 클릭하세요. 이 문제를 방지하려면 원격 기기에 RustDesk를 설치하는 것이 좋습니다."), + ("Please wait for confirmation of UAC...", "UAC 확인을 기다려주세요..."), + ("elevated_foreground_window_tip", "원격 데스크톱의 현재 창을 작동하려면 더 높은 권한이 필요하므로 일시적으로 마우스와 키보드를 사용할 수 없습니다. 원격 사용자에게 현재 창을 최소화하도록 요청하거나 연결 관리 창에서 권한 상승 버튼을 클릭할 수 있습니다. 이 문제를 방지하려면 원격 장치에 소프트웨어를 설치하는 것이 좋습니다."), ("Disconnected", "연결 끊김"), ("Other", "기타"), ("Confirm before closing multiple tabs", "여러 탭을 닫기 전에 확인"), ("Keyboard Settings", "키보드 설정"), - ("Full Access", "전체 권한"), + ("Full Access", "전체 액세스"), ("Screen Share", "화면 공유"), ("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 데스크톱 환경을 사용하거나 OS를 변경해 주세요."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland는 상위 버전의 Linux 배포판이 필요합니다. X11 데스크톱을 사용하거나 OS를 변경하세요."), ("JumpLink", "점프 링크"), - ("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하세요 (상대방 기기에서 선택)."), - ("Show RustDesk", "RustDesk 창 표시"), + ("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하세요 (피어 측에서 작동)"), + ("Show RustDesk", "RustDesk 표시"), ("This PC", "이 PC"), ("or", "또는"), - ("Continue with", "~(으)로 계속"), + ("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", "기기 접근 권한을 요청합니다."), + ("Request access to your device", "장치에 대한 액세스 권한을 요청"), ("Hide connection management window", "연결 관리 창 숨기기"), - ("hide_cm_tip", "연결 관리 창 숨기기 기능은 영구 비밀번호를 사용하는 연결에만 적용됩니다."), - ("wayland_experiment_tip", "Wayland 지원은 실험 단계 기능입니다. 무인 액세스가 필요하면 X11을 사용하세요."), - ("Right click to select tabs", "마우스 오른쪽 버튼으로 탭 선택"), + ("hide_cm_tip", "비밀번호를 통해 세션을 수락하고 영구 비밀번호를 사용하는 경우에만 숨기기 허용"), + ("wayland_experiment_tip", "Wayland 지원은 실험 단계에 있으며, 무인 접근이 필요한 경우 X11을 사용해 주세요."), + ("Right click to select tabs", "마우스 오른쪽 버튼을 클릭하여 탭 선택"), ("Skipped", "건너뜀"), ("Add to address book", "주소록에 추가"), ("Group", "그룹"), ("Search", "검색"), - ("Closed manually by web console", "웹 콘솔에서 수동으로 연결을 종료했습니다."), + ("Closed manually by web console", "웹 콘솔에 의해 수동으로 닫힘"), ("Local keyboard type", "로컬 키보드 유형"), ("Select local keyboard type", "로컬 키보드 유형 선택"), - ("software_render_tip", "Nvidia 그래픽 카드 사용 시 연결 후 원격 창이 바로 닫힌다면, nouveau 드라이버를 설치하고 소프트웨어 렌더링을 사용해 보세요. 변경 사항을 적용하려면 프로그램을 다시 시작해야 합니다."), + ("software_render_tip", "Linux에서 Nvidia 그래픽 카드를 사용 중인데 원격 창이 연결 즉시 닫히는 경우 오픈 소스 Nouveau 드라이버로 전환하고 소프트웨어 렌더링을 사용하기로 선택하는 것이 도움이 될 수 있습니다. 소프트웨어를 재시작해야 합니다."), ("Always use software rendering", "항상 소프트웨어 렌더링 사용"), - ("config_input", "키보드로 원격 데스크톱을 제어하려면 RustDesk에 '입력 모니터링' 권한을 허용해 주세요."), - ("config_microphone", "마이크로 오디오를 전송하려면 RustDesk에 '오디오 녹음' 권한을 허용해 주세요."), - ("request_elevation_tip", "상대방이 관리자 권한 상승을 요청할 수 있습니다."), + ("config_input", "키보드로 원격 데스크톱을 제어하려면 RustDesk에 \"입력 모니터링\" 권한을 부여해야 합니다."), + ("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", "원격 사용자는 RustDesk를 실행하는 UAC(사용자 계정 컨트롤) 창에서 '예'를 클릭해야 합니다."), + ("Choose this if the remote account is administrator", "원격 계정이 관리자인 경우 이 옵션을 선택합니다"), + ("Transmit the username and password of administrator", "관리자의 사용자 이름과 비밀번호 전송"), + ("still_click_uac_tip", "여전히 원격 사용자가 RustDesk를 실행하는 UAC 창에서 확인을 클릭해야 합니다."), ("Request Elevation", "권한 상승 요청"), - ("wait_accept_uac_tip", "원격 사용자가 UAC(사용자 계정 컨트롤) 대화 상자를 확인할 때까지 기다려 주세요."), - ("Elevate successfully", "관리자 권한으로 실행되었습니다."), + ("wait_accept_uac_tip", "원격 사용자가 UAC 대화 상자를 수락할 때까지 기다리세요."), + ("Elevate successfully", "권한 상승이 성공하였습니다"), ("uppercase", "대문자"), ("lowercase", "소문자"), ("digit", "숫자"), @@ -430,8 +430,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "약함"), ("Medium", "보통"), ("Strong", "강력"), - ("Switch Sides", "제어 방향 전환"), - ("Please confirm if you want to share your desktop?", "데스크톱 화면을 공유하시겠습니까?"), + ("Switch Sides", "측면 전환"), + ("Please confirm if you want to share your desktop?", "데스크탑을 공유하시겠습니까?"), ("Display", "디스플레이"), ("Default View Style", "기본 보기 스타일"), ("Default Scroll Style", "기본 스크롤 스타일"), @@ -444,11 +444,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "음성 통화"), ("Text chat", "텍스트 채팅"), ("Stop voice call", "음성 통화 종료"), - ("relay_hint_tip", "직접 연결이 어려울 경우 릴레이 서버를 통해 연결해 보세요. \nID 뒤에 '/r'을 추가하여 바로 릴레이 연결을 사용하거나, 최근 연결 목록의 항목에서 릴레이 연결을 강제할 수 있습니다."), + ("relay_hint_tip", "직접 연결이 불가능할 수 있으며 릴레이를 통해 연결을 시도할 수 있습니다. 또한 첫 번째 시도에서 릴레이를 사용하려면 아이디에 \"/r\" 접미사를 추가하거나 최근 세션 카드에 \"항상 릴레이를 통해 연결\" 옵션이 있는 경우 이 옵션을 선택하면 됩니다."), ("Reconnect", "다시 연결"), ("Codec", "코덱"), ("Resolution", "해상도"), - ("No transfers in progress", "진행 중인 전송이 없습니다."), + ("No transfers in progress", "진행 중인 전송이 없습니다"), ("Set one-time password length", "일회용 비밀번호 길이 설정"), ("RDP Settings", "RDP 설정"), ("Sort by", "정렬 기준"), @@ -456,252 +456,252 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restore", "복원"), ("Minimize", "최소화"), ("Maximize", "최대화"), - ("Your Device", "내 기기"), - ("empty_recent_tip", "최근 연결 기록이 없습니다. 새 연결을 시작해 보세요."), - ("empty_favorite_tip", "즐겨찾는 기기가 없습니다. 새 즐겨찾기를 추가해 보세요."), - ("empty_lan_tip", "LAN 내에서 검색된 기기가 없습니다."), - ("empty_address_book_tip", "주소록에 등록된 기기가 없습니다."), + ("Your Device", "내 장치"), + ("empty_recent_tip", "최근 세션이 없습니다. 새 세션을 시작해보세요"), + ("empty_favorite_tip", "장치 즐겨찾기가 없습니다. 새 즐겨찾기를 추가해보세요"), + ("empty_lan_tip", "제어되는 장치가 발견되지 않았습니다."), + ("empty_address_book_tip", "현재 주소록에 제어되는 클라이언트가 없습니다"), ("eg: admin", "예: 관리자"), - ("Empty Username", "사용자 이름이 비어 있습니다."), - ("Empty Password", "비밀번호가 비어 있습니다."), + ("Empty Username", "사용자 이름이 비어있습니다"), + ("Empty Password", "비밀번호가 비어있습니다"), ("Me", "나"), ("identical_file_tip", "이 파일은 상대방의 파일과 일치합니다."), - ("show_monitors_tip", "도구 모음에 모니터 목록 표시"), + ("show_monitors_tip", "도구 모음에 모니터 표시"), ("View Mode", "보기 모드"), - ("login_linux_tip", "X 데스크톱 세션을 시작하려면 원격 Linux 시스템 계정으로 로그인하세요."), + ("login_linux_tip", "X 데스크탑을 활성화하려면 제어되는 터미널의 Linux 계정에 로그인하세요"), ("verify_rustdesk_password_tip", "RustDesk 비밀번호 확인"), ("remember_account_tip", "이 계정 기억하기"), - ("os_account_desk_tip", "모니터가 없는(헤드리스) 환경에서 이 계정으로 원격 시스템에 로그인하여 데스크톱 세션을 활성화할 수 있습니다."), + ("os_account_desk_tip", "이 계정은 원격 OS에 로그인하고 헤드리스에서 데스크톱 세션을 활성화하는 데 사용됩니다."), ("OS Account", "OS 계정"), - ("another_user_login_title_tip", "다른 사용자가 로그인되어 있습니다"), - ("another_user_login_text_tip", "연결 종료"), - ("xorg_not_found_title_tip", "Xorg가 설치되지 않았습니다"), - ("xorg_not_found_text_tip", "Xorg를 설치해 주세요."), - ("no_desktop_title_tip", "데스크톱 환경이 설치되지 않았습니다."), - ("no_desktop_text_tip", "데스크톱 환경을 설치해 주세요."), - ("No need to elevate", "권한 상승이 필요하지 않습니다."), - ("System Sound", "시스템 사운드"), + ("another_user_login_title_tip", "다른 사용자가 이미 로그인했습니다"), + ("another_user_login_text_tip", "연결 끊기"), + ("xorg_not_found_title_tip", "Xorg를 찾을 수 없습니다"), + ("xorg_not_found_text_tip", "Xorg를 설치해 주세요"), + ("no_desktop_title_tip", "사용 가능한 데스크톱 환경이 없습니다"), + ("no_desktop_text_tip", "GNOME 데스크톱을 설치해 주세요"), + ("No need to elevate", "권한 상승이 필요없습니다"), + ("System Sound", "시스템 소리"), ("Default", "기본"), - ("New RDP", "새로운 RDP"), + ("New RDP", "새 RDP"), ("Fingerprint", "지문"), ("Copy Fingerprint", "지문 복사"), ("no fingerprints", "지문이 없습니다"), - ("Select a peer", "상대방 선택"), - ("Select peers", "상대방 선택 (복수)"), + ("Select a peer", "피어 선택"), + ("Select peers", "피어 선택"), ("Plugins", "플러그인"), - ("Uninstall", "제거"), + ("Uninstall", "설치 제거"), ("Update", "업데이트"), - ("Enable", "활성화"), - ("Disable", "비활성화"), + ("Enable", "사용함"), + ("Disable", "사용 안 함"), ("Options", "옵션"), ("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", "발신 연결"), + ("accept_and_elevate_btn_tooltip", "연결을 수락하고 UAC 권한을 높입니다."), + ("clipboard_wait_response_timeout_tip", "복사 응답을 기다리는 동안 시간이 초과되었습니다."), + ("Incoming connection", "들어오는 연결"), + ("Outgoing connection", "나가는 연결"), ("Exit", "종료"), ("Open", "열기"), - ("logout_tip", "정말 로그아웃하시겠습니까?"), + ("logout_tip", "로그아웃하시겠습니까?"), ("Service", "서비스"), ("Start", "시작"), ("Stop", "중지"), - ("exceed_max_devices", "관리 중인 기기 수가 최대치에 도달했습니다."), - ("Sync with recent sessions", "최근 연결 기록과 동기화"), + ("exceed_max_devices", "관리되는 장치의 최대 수에 도달했습니다."), + ("Sync with recent sessions", "최근 세션과 동기화"), ("Sort tags", "태그 정렬"), ("Open connection in new tab", "새 탭에서 연결 열기"), - ("Move tab to new window", "탭을 새 창으로 이동"), + ("Move tab to new window", "새 창으로 탭 이동"), ("Can not be empty", "비워둘 수 없습니다"), - ("Already exists", "이미 존재합니다."), + ("Already exists", "이미 존재합니다"), ("Change Password", "비밀번호 변경"), - ("Refresh Password", "비밀번호 새로고침"), + ("Refresh Password", "비밀번호 새로 고침"), ("ID", "ID"), - ("Grid View", "그리드 보기"), - ("List View", "리스트 보기"), + ("Grid View", "격자 보기"), + ("List View", "목록 보기"), ("Select", "선택"), ("Toggle Tags", "태그 전환"), - ("pull_ab_failed_tip", "주소록을 가져오지 못했습니다."), - ("push_ab_failed_tip", "주소록 업로드 실패"), - ("synced_peer_readded_tip", "최근 연결 기록에 있는 기기는 주소록에 다시 동기화됩니다."), + ("pull_ab_failed_tip", "주소록을 새로 고치지 못했습니다"), + ("push_ab_failed_tip", "주소록을 서버에 동기화하지 못했습니다"), + ("synced_peer_readded_tip", "최근 세션에 있던 장치들이 주소록으로 다시 동기화될 것입니다."), ("Change Color", "색상 변경"), ("Primary Color", "기본 색상"), ("HSV Color", "HSV 색상"), - ("Installation Successful!", "설치가 완료되었습니다."), - ("Installation failed!", "설치에 실패했습니다."), + ("Installation Successful!", "설치에 성공했습니다!"), + ("Installation failed!", "설치에 실패했습니다!"), ("Reverse mouse wheel", "마우스 휠 반전"), ("{} sessions", "{} 세션"), - ("scam_title", "사기 피해에 주의하세요!"), - ("scam_text1", "모르는 사람이 RustDesk 사용을 요청하며 접근하는 경우, 사기일 수 있으니 즉시 연결을 종료하세요."), - ("scam_text2", "금전 또는 개인 정보를 탈취하려는 사기꾼일 가능성이 높습니다."), + ("scam_title", "사기를 당하고 있을 수 있습니다!"), + ("scam_text1", "알지 못하고 신뢰할 수 없는 사람이 전화를 걸어 RustDesk를 사용하고 서비스를 시작하라고 요청하는 경우 계속 진행하지 말고 즉시 전화를 끊으세요."), + ("scam_text2", "사기꾼이 귀하의 돈이나 기타 개인 정보를 훔치려 할 가능성이 높습니다."), ("Don't show again", "다시 표시하지 않음"), - ("I Agree", "동의합니다"), + ("I Agree", "동의"), ("Decline", "거절"), - ("Timeout in minutes", "시간 초과(분)"), - ("auto_disconnect_option_tip", "비활성 시 연결 자동 종료"), - ("Connection failed due to inactivity", "장시간 활동이 없어 연결이 자동으로 종료되었습니다"), + ("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", "연결 수락 시 배경화면 제거"), + ("pull_group_failed_tip", "그룹 새로 고침에 실패했습니다"), + ("Filter by intersection", "교차해서 필터링"), + ("Remove wallpaper during incoming sessions", "들어오는 세션 동안 배경화면 제거"), ("Test", "테스트"), - ("display_is_plugged_out_msg", "디스플레이 연결이 해제되었습니다. 첫 번째 디스플레이로 전환하세요."), + ("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", "SELinux가 활성화된 경우 RustDesk가 호스트로 제대로 작동하지 않을 수 있습니다."), + ("Use all my displays for the remote session", "원격 세션에 내 모든 디스플레이 사용"), + ("selinux_tip", "SELinux가 장치에서 활성화되어 있어 RustDesk가 제어된 상태로 제대로 작동하지 않을 수 있습니다."), ("Change view", "보기 변경"), ("Big tiles", "큰 타일"), ("Small tiles", "작은 타일"), - ("List", "리스트"), + ("List", "목록"), ("Virtual display", "가상 디스플레이"), - ("Plug out all", "모든 가상 디스플레이 연결 끊기"), + ("Plug out all", "모든 플러그를 뽑으세요"), ("True color (4:4:4)", "트루컬러 (4:4:4)"), - ("Enable blocking user input", "원격 사용자 입력 차단 활성화"), - ("id_input_tip", "ID, IP 주소 또는 도메인과 포트(:)를 입력하세요.\n다른 서버의 기기에 연결하려면 서버 주소(@?key=)를 입력하세요."), + ("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\"."), ("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", "간접 디스플레이 드라이버(IDD)는 Windows 10 버전 2004 이상에서 지원됩니다."), + ("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", "Control 및 Command 키 교체"), - ("swap-left-right-mouse", "마우스 왼쪽 버튼과 오른쪽 버튼 바꾸기"), - ("2FA code", "2단계 인증 코드"), - ("More", "더보기"), - ("enable-2fa-title", "2단계 인증 활성화"), - ("enable-2fa-desc", "지금 인증 앱을 설정하십시오. Authy, Microsoft/Google Authenticator와 같은 모바일 또는 데스크톱 인증 앱을 사용할 수 있습니다. QR 코드를 스캔한 후, 인증 앱에 표시된 코드를 입력하면 2단계 인증이 활성화됩니다."), - ("wrong-2fa-code", "코드를 확인할 수 없습니다. 인증 코드와 기기 시간이 정확한지 확인해 주십시오."), - ("enter-2fa-title", "2단계 인증"), + ("swap-left-right-mouse", "마우스 왼쪽 버튼과 오른쪽 버튼 교체"), + ("2FA code", "이중 인증 코드"), + ("More", "더 보기"), + ("enable-2fa-title", "이중 인증 사용함"), + ("enable-2fa-desc", "지금 인증앱을 설정해 주세요. 휴대폰이나 데스크톱에서 Authy, Microsoft 또는 Google 인증기와 같은 인증기 앱을 사용할 수 있습니다.\n\n앱으로 QR 코드를 스캔하고 앱에 표시된 코드를 입력하면 이중 인증이 가능합니다."), + ("wrong-2fa-code", "코드를 확인할 수 없습니다. 코드와 현지 시간 설정이 올바른지 확인합니다"), + ("enter-2fa-title", "이중 인증"), ("Email verification code must be 6 characters.", "이메일 인증 코드는 6자여야 합니다."), - ("2FA code must be 6 digits.", "2단계 인증 코드는 6자리여야 합니다."), - ("Multiple Windows sessions found", "여러 Windows 세션이 발견되었습니다."), - ("Please select the session you want to connect to", "연결하려는 세션을 선택해 주십시오."), + ("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", "공유 비밀번호 설정"), - ("Exist in", "다음 위치에 존재:"), + ("Exist in", "다음 위치 존재"), ("Read-only", "읽기 전용"), ("Read/Write", "읽기/쓰기"), ("Full Control", "전체 제어"), - ("share_warning_tip", "위에 선택한 항목은 다른 사용자와 공유되어 접근할 수 있습니다."), + ("share_warning_tip", "위의 필드는 공유되고 다른 사람들에게 보입니다."), ("Everyone", "모두"), - ("ab_web_console_tip", "웹 콘솔 자세히 알아보기"), - ("allow-only-conn-window-open-tip", "RustDesk 창이 열려 있는 경우에만 연결 허용"), - ("no_need_privacy_mode_no_physical_displays_tip", "물리적 디스플레이가 없는 경우 프라이버시 모드를 사용할 필요가 없습니다."), + ("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입니다."), + ("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", "화면 선택을 취소하고 공유할 화면을 다시 선택할 수 있습니다."), + ("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", "텍스처 렌더링은 이미지 품질을 향상시킵니다. 렌더링 문제가 발생하면 이 옵션을 비활성화하십시오."), + ("android_new_voice_call_tip", "새 음성 통화 요청이 수신되었습니다. 수락하면 오디오가 음성 통신으로 전환됩니다."), + ("texture_render_tip", "텍스처 렌더링을 사용하여 사진을 더 부드럽게 만듭니다. 렌더링 문제가 발생하면 이 옵션을 비활성화할 수 있습니다."), ("Use texture rendering", "텍스처 렌더링 사용"), ("Floating window", "플로팅 창"), - ("floating_window_tip", "RustDesk 백그라운드 서비스 유지를 권장합니다."), + ("floating_window_tip", "RustDesk 백그라운드 서비스를 유지하는 데 도움이 됩니다"), ("Keep screen on", "화면 켜짐 유지"), ("Never", "없음"), - ("During controlled", "원격 제어 중"), - ("During service is on", "서비스 실행 중"), - ("Capture screen using DirectX", "DirectX로 화면 캡처"), + ("During controlled", "제어되는 동안"), + ("During service is on", "서비스 중"), + ("Capture screen using DirectX", "DirectX를 사용하여 화면 캡처"), ("Back", "뒤로"), ("Apps", "앱"), ("Volume up", "볼륨 높이기"), ("Volume down", "볼륨 낮추기"), ("Power", "전원"), - ("Telegram bot", "텔레그램 봇"), - ("enable-bot-tip", "이 기능을 활성화하면 텔레그램 봇으로 2단계 인증 코드를 받고 연결 알림도 받을 수 있습니다."), - ("enable-bot-desc", "1. @BotFather와 대화를 시작하십시오.\n2. '/newbot' 명령을 보내 토큰을 받으십시오.\n3. 새로 만든 봇과 대화를 시작하고 '/hello' 같은 명령을 보내 봇을 활성화하십시오."), - ("cancel-2fa-confirm-tip", "2단계 인증을 정말 취소하시겠습니까?"), - ("cancel-bot-confirm-tip", "텔레그램 봇을 정말 삭제하시겠습니까?"), + ("Telegram bot", "Telegram 봇"), + ("enable-bot-tip", "이 기능을 활성화하면 봇에서 이중 인중 코드를 받을 수 있습니다. 또한 연결 알림 기능도 할 수 있습니다."), + ("enable-bot-desc", "1. @BotFather와 채팅을 시작합니다.\n2. \"/newbot\" 명령을 보내주세요. 이 단계를 완료하면 토큰을 받게 됩니다.\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", "최소 {}자 이상 입력해야 합니다."), + ("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", "신뢰하는 기기에서 2단계 인증 건너뛰기"), - ("Parent directory", "상위 디렉토리"), + ("Days remaining", "일 남음"), + ("enable-trusted-devices-tip", "신뢰할 수 있는 장치에서 이중 인증 건너뛰기"), + ("Parent directory", "상위 디렉터리"), ("Resume", "재개"), ("Invalid file name", "잘못된 파일 이름"), - ("one-way-file-transfer-tip", "단방향 파일 전송은 원격 제어 대상 기기에서 활성화해야 합니다."), - ("Authentication Required", "인증이 필요합니다."), + ("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'을 입력하십시오. (공용 서버는 키가 필요 없습니다.)"), + ("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", "클립보드가 동기화되었습니다."), + ("Clipboard is synchronized", "클립보드가 동기화되었습니다"), ("Update client clipboard", "클라이언트 클립보드 업데이트"), ("Untagged", "태그 없음"), - ("new-version-of-{}-tip", "{}의 새 버전이 출시되었습니다."), - ("Accessible devices", "연결 가능한 기기"), - ("upgrade_remote_rustdesk_client_to_{}_tip", "원격 기기의 RustDesk 클라이언트를 {} 버전 이상으로 업그레이드하십시오!"), - ("d3d_render_tip", "D3D 렌더링을 활성화하면 일부 기기에서 원격 화면이 표시되지 않을 수 있습니다."), - ("Use D3D rendering", "D3D 렌더링 활성화"), + ("new-version-of-{}-tip", "{}의 새 버전을 사용할 수 있습니다"), + ("Accessible devices", "액세스 가능한 장치"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "RustDesk 클라이언트를 원격 버전 {} 이상으로 업그레이드해 주세요!"), + ("d3d_render_tip", "D3D 렌더링이 활성화되면 일부 기기에서는 원격 화면이 검은색으로 표시될 수 있습니다."), + ("Use D3D rendering", "D3D 렌더링 사용"), ("Printer", "프린터"), - ("printer-os-requirement-tip", "프린터 출력 기능은 Windows 10 이상에서 지원됩니다."), - ("printer-requires-installed-{}-client-tip", "원격 인쇄 기능을 사용하려면 이 기기에 {}를 설치해야 합니다."), + ("printer-os-requirement-tip", "프린터 출력 기능은 Windows 10 이상이 필요합니다."), + ("printer-requires-installed-{}-client-tip", "원격 인쇄 기능을 사용하려면 이 장치에 {}를 설치해야 합니다."), ("printer-{}-not-installed-tip", "{} 프린터가 설치되지 않았습니다."), - ("printer-{}-ready-tip", "{} 프린터가 설치되었습니다. 인쇄 기능을 사용할 수 있습니다."), + ("printer-{}-ready-tip", "{} 프린터가 설치되어 사용할 준비가 되었습니다."), ("Install {} Printer", "{} 프린터 설치"), - ("Outgoing Print Jobs", "보낸 인쇄 작업"), - ("Incoming Print Jobs", "받은 인쇄 작업"), - ("Incoming Print Job", "받은 인쇄 작업"), + ("Outgoing Print Jobs", "나가는 인쇄 작업"), + ("Incoming Print Jobs", "들어오는 인쇄 작업"), + ("Incoming Print Job", "들어오는 인쇄 작업"), ("use-the-default-printer-tip", "기본 프린터 사용"), ("use-the-selected-printer-tip", "선택한 프린터 사용"), - ("auto-print-tip", "선택한 프린터로 자동 인쇄"), - ("print-incoming-job-confirm-tip", "원격 인쇄 작업을 받았습니다. 인쇄하시겠습니까?"), - ("remote-printing-disallowed-tile-tip", "원격 인쇄 비허용"), - ("remote-printing-disallowed-text-tip", "원격 제어 대상 기기의 권한 설정에서 원격 인쇄가 거부되었습니다."), + ("auto-print-tip", "선택한 프린터를 사용하여 자동으로 인쇄합니다."), + ("print-incoming-job-confirm-tip", "원격에서 인쇄 작업을 받았습니다. 옆에서 실행하시겠습니까?"), + ("remote-printing-disallowed-tile-tip", "원격 인쇄 허용 안 함"), + ("remote-printing-disallowed-text-tip", "제어측의 권한 설정에서 원격 인쇄를 거부합니다."), ("save-settings-tip", "설정 저장"), ("dont-show-again-tip", "다시 표시하지 않음"), - ("Take screenshot", "스크린샷 캡처"), - ("Taking screenshot", "스크린샷 저장 중"), - ("screenshot-merged-screen-not-supported-tip", "다중 디스플레이 화면 병합 스크린샷은 현재 지원되지 않습니다. 단일 디스플레이로 전환한 후 다시 시도하십시오."), - ("screenshot-action-tip", "스크린샷 저장 방식을 선택하십시오."), + ("Take screenshot", "스크린샷 찍기"), + ("Taking screenshot", "스크린샷 찍는 중"), + ("screenshot-merged-screen-not-supported-tip", "현재 다중 디스플레이의 스크린샷 병합이 지원되지 않습니다. 단일 디스플레이로 전환한 후 다시 시도해 주세요."), + ("screenshot-action-tip", "스크린샷을 계속 진행할 방법을 선택해 주세요."), ("Save as", "다른 이름으로 저장"), ("Copy to clipboard", "클립보드에 복사"), - ("Enable remote printer", "원격 프린터 활성화"), + ("Enable remote printer", "원격 프린터 사용함"), ("Downloading {}", "{} 다운로드 중"), ("{} Update", "{} 업데이트"), - ("{}-to-update-tip", ""), - ("download-new-version-failed-tip", "다운로드에 실패했습니다. 다시 시도하거나 '다운로드' 버튼을 클릭하여 릴리스 페이지에서 직접 다운로드한 후 수동으로 업그레이드하십시오."), + ("{}-to-update-tip", "{}가 지금 닫히고 새 버전을 설치합니다."), + ("download-new-version-failed-tip", "다운로드에 실패했습니다. 다시 시도하거나 \"다운로드\" 버튼을 클릭하여 릴리스 페이지에서 다운로드하고 수동으로 업그레이드할 수 있습니다."), ("Auto update", "자동 업데이트"), - ("update-failed-check-msi-tip", "설치 방법을 확인할 수 없습니다. '다운로드' 버튼을 클릭하여 릴리스 페이지에서 직접 다운로드한 후 수동으로 업그레이드하십시오."), - ("websocket_tip", "WebSocket 사용 시 릴레이 연결만 지원됩니다."), + ("update-failed-check-msi-tip", "설치 방법 확인에 실패했습니다. \"다운로드\" 버튼을 클릭하여 릴리스 페이지에서 다운로드하고 수동으로 업그레이드하세요."), + ("websocket_tip", "WebSocket을 사용할 때는 릴레이 연결만 지원됩니다."), ("Use WebSocket", "웹소켓 사용"), ("Trackpad speed", "트랙패드 속도"), ("Default trackpad speed", "기본 트랙패드 속도"), - ("Numeric one-time password", "일회용 비밀번호"), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("Numeric one-time password", "숫자 일회용 비밀번호"), + ("Enable IPv6 P2P connection", "IPv6 P2P 연결 사용"), + ("Enable UDP hole punching", "UDP 홀 펀칭 사용"), ("View camera", "카메라 보기"), - ("Enable camera", "카메라 보기 허용"), + ("Enable camera", "카메라 사용함"), ("No cameras", "카메라 없음"), - ("view_camera_unsupported_tip", "원격 기기에서 카메라 보기를 지원하지 않습니다."), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), + ("view_camera_unsupported_tip", "원격 장치가 카메라 보기를 지원하지 않습니다."), + ("Terminal", "터미널"), + ("Enable terminal", "터미널 사용함"), + ("New tab", "새 탭"), + ("Keep terminal sessions on disconnect", "터미널 세션 연결 해제 상태 유지"), ].iter().cloned().collect(); } From abb7748ee92d39998e702695fb85b4c02c6bc213 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:32:14 +0800 Subject: [PATCH 403/506] refact: terminal, win, run as admin (#12300) Signed-off-by: fufesou --- Cargo.lock | 8 +- Cargo.toml | 2 +- flutter/lib/common.dart | 17 +- flutter/lib/common/widgets/dialog.dart | 67 ++++++-- flutter/lib/common/widgets/peer_card.dart | 38 ++++- flutter/lib/mobile/pages/home_page.dart | 6 + flutter/lib/models/model.dart | 14 +- src/client.rs | 33 +++- src/flutter_ffi.rs | 40 ++++- src/lang/ar.rs | 7 + src/lang/be.rs | 7 + src/lang/bg.rs | 7 + src/lang/ca.rs | 7 + src/lang/cn.rs | 7 + src/lang/cs.rs | 7 + src/lang/da.rs | 7 + src/lang/de.rs | 7 + src/lang/el.rs | 7 + src/lang/en.rs | 1 + src/lang/eo.rs | 7 + src/lang/es.rs | 7 + src/lang/et.rs | 7 + src/lang/eu.rs | 7 + src/lang/fa.rs | 7 + src/lang/fr.rs | 7 + src/lang/ge.rs | 7 + src/lang/he.rs | 7 + src/lang/hr.rs | 7 + src/lang/hu.rs | 7 + src/lang/id.rs | 7 + src/lang/it.rs | 7 + src/lang/ja.rs | 7 + src/lang/ko.rs | 7 + src/lang/kz.rs | 7 + src/lang/lt.rs | 7 + src/lang/lv.rs | 7 + src/lang/nb.rs | 7 + src/lang/nl.rs | 7 + src/lang/pl.rs | 7 + src/lang/pt_PT.rs | 7 + src/lang/ptbr.rs | 7 + src/lang/ro.rs | 7 + src/lang/ru.rs | 7 + src/lang/sc.rs | 7 + src/lang/sk.rs | 7 + src/lang/sl.rs | 7 + src/lang/sq.rs | 7 + src/lang/sr.rs | 7 + src/lang/sv.rs | 7 + src/lang/ta.rs | 7 + src/lang/template.rs | 7 + src/lang/th.rs | 7 + src/lang/tr.rs | 7 + src/lang/tw.rs | 7 + src/lang/uk.rs | 7 + src/lang/vi.rs | 7 + src/platform/windows.rs | 187 +++++++++++++++++++++- src/server/connection.rs | 164 ++++++++++++++++++- src/server/terminal_service.rs | 63 +++++++- 59 files changed, 920 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20e1b34ab7f..d73b36b09fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2218,9 +2218,8 @@ dependencies = [ [[package]] name = "filedescriptor" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +version = "0.8.2" +source = "git+https://github.com/rustdesk-org/wezterm?branch=rustdesk/pty_based_0.8.1#80174f8009f41565f0fa8c66dab90d4f9211ae16" dependencies = [ "libc", "thiserror 1.0.61", @@ -5233,8 +5232,7 @@ dependencies = [ [[package]] name = "portable-pty" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806ee80c2a03dbe1a9fb9534f8d19e4c0546b790cde8fd1fea9d6390644cb0be" +source = "git+https://github.com/rustdesk-org/wezterm?branch=rustdesk/pty_based_0.8.1#80174f8009f41565f0fa8c66dab90d4f9211ae16" dependencies = [ "anyhow", "bitflags 1.3.2", diff --git a/Cargo.toml b/Cargo.toml index 06bfcaeb303..da8c3bff0e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,7 @@ ctrlc = "3.2" # arboard = { version = "3.4", features = ["wayland-data-control"] } arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] } clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" } -portable-pty = "0.8.1" # higher version not work on rustc 1.75 +portable-pty = { git = "https://github.com/rustdesk-org/wezterm", branch = "rustdesk/pty_based_0.8.1", package = "portable-pty" } system_shutdown = "4.0" qrcode-generator = "4.1" diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0fc8aa6c0de..f54b88e88da 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2124,6 +2124,10 @@ enum UriLinkType { terminal, } +setEnvTerminalAdmin() { + bind.mainSetEnv(key: 'IS_TERMINAL_ADMIN', value: 'Y'); +} + // uri link handler bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { List? args; @@ -2191,6 +2195,12 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { id = args[i + 1]; i++; break; + case '--terminal-admin': + setEnvTerminalAdmin(); + type = UriLinkType.terminal; + id = args[i + 1]; + i++; + break; case '--password': password = args[i + 1]; i++; @@ -2264,7 +2274,8 @@ List? urlLinkToCmdArgs(Uri uri) { "view-camera", "port-forward", "rdp", - "terminal" + "terminal", + "terminal-admin", ]; if (uri.authority.isEmpty && uri.path.split('').every((char) => char == '/')) { @@ -2334,6 +2345,10 @@ List? urlLinkToCmdArgs(Uri uri) { } else if (command == '--terminal') { connect(Get.context!, id, isTerminal: true, forceRelay: forceRelay, password: password); + } else if (command == 'terminal-admin') { + setEnvTerminalAdmin(); + connect(Get.context!, id, + isTerminal: true, forceRelay: forceRelay, password: password); } else { // Default to remote desktop for '--connect', '--play', or direct connection connect(Get.context!, id, forceRelay: forceRelay, password: password); diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 7c75f96f722..fc2334d58b1 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -819,23 +819,33 @@ void enterPasswordDialog( } void enterUserLoginDialog( - SessionID sessionId, OverlayDialogManager dialogManager) async { + SessionID sessionId, + OverlayDialogManager dialogManager, + String osAccountDescTip, + bool canRememberAccount) async { await _connectDialog( sessionId, dialogManager, osUsernameController: TextEditingController(), osPasswordController: TextEditingController(), + osAccountDescTip: osAccountDescTip, + canRememberAccount: canRememberAccount, ); } void enterUserLoginAndPasswordDialog( - SessionID sessionId, OverlayDialogManager dialogManager) async { + SessionID sessionId, + OverlayDialogManager dialogManager, + String osAccountDescTip, + bool canRememberAccount) async { await _connectDialog( sessionId, dialogManager, osUsernameController: TextEditingController(), osPasswordController: TextEditingController(), passwordController: TextEditingController(), + osAccountDescTip: osAccountDescTip, + canRememberAccount: canRememberAccount, ); } @@ -845,17 +855,28 @@ _connectDialog( TextEditingController? osUsernameController, TextEditingController? osPasswordController, TextEditingController? passwordController, + String? osAccountDescTip, + bool canRememberAccount = true, }) async { + final errUsername = ''.obs; var rememberPassword = false; if (passwordController != null) { rememberPassword = await bind.sessionGetRemember(sessionId: sessionId) ?? false; } var rememberAccount = false; - if (osUsernameController != null) { + if (canRememberAccount && osUsernameController != null) { rememberAccount = await bind.sessionGetRemember(sessionId: sessionId) ?? false; } + if (osUsernameController != null) { + osUsernameController.addListener(() { + if (errUsername.value.isNotEmpty) { + errUsername.value = ''; + } + }); + } + dialogManager.dismissAll(); dialogManager.show((setState, close, context) { cancel() { @@ -864,6 +885,13 @@ _connectDialog( } submit() { + if (osUsernameController != null) { + if (osUsernameController.text.trim().isEmpty) { + errUsername.value = translate('Empty Username'); + setState(() {}); + return; + } + } final osUsername = osUsernameController?.text.trim() ?? ''; final osPassword = osPasswordController?.text.trim() ?? ''; final password = passwordController?.text.trim() ?? ''; @@ -927,26 +955,39 @@ _connectDialog( } return Column( children: [ - descWidget(translate('login_linux_tip')), + if (osAccountDescTip != null) descWidget(translate(osAccountDescTip)), DialogTextField( title: translate(DialogTextField.kUsernameTitle), controller: osUsernameController, prefixIcon: DialogTextField.kUsernameIcon, errorText: null, ), + if (errUsername.value.isNotEmpty) + Align( + alignment: Alignment.centerLeft, + child: SelectableText( + errUsername.value, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + textAlign: TextAlign.left, + ).paddingOnly(left: 12, bottom: 2), + ), PasswordWidget( controller: osPasswordController, autoFocus: false, ), - rememberWidget( - translate('remember_account_tip'), - rememberAccount, - (v) { - if (v != null) { - setState(() => rememberAccount = v); - } - }, - ), + if (canRememberAccount) + rememberWidget( + translate('remember_account_tip'), + rememberAccount, + (v) { + if (v != null) { + setState(() => rememberAccount = v); + } + }, + ), ], ); } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index d664f3d801a..4b52e6c46d0 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -492,6 +492,7 @@ abstract class BasePeerCard extends StatelessWidget { bool isTcpTunneling = false, bool isRDP = false, bool isTerminal = false, + bool isTerminalRunAsAdmin = false, }) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( @@ -499,6 +500,9 @@ abstract class BasePeerCard extends StatelessWidget { style: style, ), proc: () { + if (isTerminalRunAsAdmin) { + setEnvTerminalAdmin(); + } connectInPeerTab( context, peer, @@ -507,7 +511,7 @@ abstract class BasePeerCard extends StatelessWidget { isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, isRDP: isRDP, - isTerminal: isTerminal, + isTerminal: isTerminal || isTerminalRunAsAdmin, ); }, padding: menuPadding, @@ -552,6 +556,15 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + MenuEntryBase _terminalRunAsAdminAction(BuildContext context) { + return _connectCommonAction( + context, + translate('Terminal (Run as administrator)'), + isTerminalRunAsAdmin: true, + ); + } + @protected MenuEntryBase _tcpTunnelingAction(BuildContext context) { return _connectCommonAction( @@ -906,6 +919,10 @@ class RecentPeerCard extends BasePeerCard { _terminalAction(context), ]; + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + final List favs = (await bind.mainGetFav()).toList(); if (isDesktop && peer.platform != kPeerPlatformAndroid) { @@ -966,6 +983,11 @@ class FavoritePeerCard extends BasePeerCard { _viewCameraAction(context), _terminalAction(context), ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); } @@ -1022,6 +1044,10 @@ class DiscoveredPeerCard extends BasePeerCard { _terminalAction(context), ]; + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + final List favs = (await bind.mainGetFav()).toList(); if (isDesktop && peer.platform != kPeerPlatformAndroid) { @@ -1076,6 +1102,11 @@ class AddressBookPeerCard extends BasePeerCard { _viewCameraAction(context), _terminalAction(context), ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); } @@ -1212,6 +1243,11 @@ class MyGroupPeerCard extends BasePeerCard { _viewCameraAction(context), _terminalAction(context), ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); } diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index e35c8872c75..651ec4f1727 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -230,6 +230,12 @@ class WebHomePage extends StatelessWidget { id = args[i + 1]; i++; break; + case '--terminal-admin': + setEnvTerminalAdmin(); + isTerminal = true; + id = args[i + 1]; + i++; + break; case '--password': password = args[i + 1]; i++; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index b151120251c..017f2c9d11b 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -836,10 +836,16 @@ class FfiModel with ChangeNotifier { } else if (type == 'input-password') { enterPasswordDialog(sessionId, dialogManager); } else if (type == 'session-login' || type == 'session-re-login') { - enterUserLoginDialog(sessionId, dialogManager); - } else if (type == 'session-login-password' || - type == 'session-login-password') { - enterUserLoginAndPasswordDialog(sessionId, dialogManager); + enterUserLoginDialog(sessionId, dialogManager, 'login_linux_tip', true); + } else if (type == 'session-login-password') { + enterUserLoginAndPasswordDialog( + sessionId, dialogManager, 'login_linux_tip', true); + } else if (type == 'terminal-admin-login') { + enterUserLoginDialog( + sessionId, dialogManager, 'terminal-admin-login-tip', false); + } else if (type == 'terminal-admin-login-password') { + enterUserLoginAndPasswordDialog( + sessionId, dialogManager, 'terminal-admin-login-tip', false); } else if (type == 'restarting') { showMsgBox(sessionId, type, title, text, link, false, dialogManager, hasCancel: false); diff --git a/src/client.rs b/src/client.rs index 4bda93d43b9..48d753756e4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1611,6 +1611,7 @@ struct ConnToken { pub struct LoginConfigHandler { id: String, pub conn_type: ConnType, + pub is_terminal_admin: bool, hash: Hash, password: Vec, // remember password for reconnect pub remember: bool, @@ -1736,6 +1737,7 @@ impl LoginConfigHandler { self.other_server = Some((real_id.to_owned(), server.to_owned(), other_server_key)); } } + self.direct = None; self.received = false; self.switch_uuid = switch_uuid; @@ -1744,6 +1746,11 @@ impl LoginConfigHandler { self.shared_password = shared_password; self.record_state = false; self.record_permission = true; + + // `std::env::remove_var("IS_TERMINAL_ADMIN");` is called in `session_add_sync()` - `flutter_ffi.rs`. + let is_terminal_admin = conn_type == ConnType::TERMINAL + && std::env::var("IS_TERMINAL_ADMIN").map_or(false, |v| v == "Y"); + self.is_terminal_admin = is_terminal_admin; } /// Check if the client should auto login. @@ -1956,7 +1963,7 @@ impl LoginConfigHandler { .into(); } else if name == keys::OPTION_TERMINAL_PERSISTENT { config.terminal_persistent.v = !config.terminal_persistent.v; - option.terminal_persistent = (if config.terminal_persistent.v { + option.terminal_persistent = (if config.terminal_persistent.v { BoolOption::Yes } else { BoolOption::No @@ -3274,6 +3281,19 @@ pub async fn handle_hash( } lc.write().unwrap().password = password.clone(); + + let is_terminal_admin = lc.read().unwrap().is_terminal_admin; + let is_terminal = lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL); + if is_terminal && is_terminal_admin { + if password.is_empty() { + interface.msgbox("terminal-admin-login-password", "", "", ""); + } else { + interface.msgbox("terminal-admin-login", "", "", ""); + } + lc.write().unwrap().hash = hash; + return; + } + let password = if password.is_empty() { // login without password, the remote side can click accept interface.msgbox("input-password", "Password Required", "", ""); @@ -3285,8 +3305,15 @@ pub async fn handle_hash( hasher.finalize()[..].into() }; - let os_username = lc.read().unwrap().get_option("os-username"); - let os_password = lc.read().unwrap().get_option("os-password"); + let is_terminal = lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL); + let (os_username, os_password) = if is_terminal { + ("".to_owned(), "".to_owned()) + } else { + ( + lc.read().unwrap().get_option("os-username"), + lc.read().unwrap().get_option("os-password"), + ) + }; send_login(lc.clone(), os_username, os_password, password, peer).await; lc.write().unwrap().hash = hash; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5a6b66a0b6d..58afae52814 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -138,7 +138,7 @@ pub fn session_add_sync( is_shared_password: bool, conn_token: Option, ) -> SyncReturn { - if let Err(e) = session_add( + let add_res = session_add( &session_id, &id, is_file_transfer, @@ -151,7 +151,14 @@ pub fn session_add_sync( password, is_shared_password, conn_token, - ) { + ); + // We can't put the remove call together with `std::env::var("IS_TERMINAL_ADMIN")`. + // Because there are some `bail!` in `session_add()`, we must make sure `IS_TERMINAL_ADMIN` is removed at last. + if is_terminal { + std::env::remove_var("IS_TERMINAL_ADMIN"); + } + + if let Err(e) = add_res { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) @@ -1067,6 +1074,35 @@ pub fn main_get_env(key: String) -> SyncReturn { SyncReturn(std::env::var(key).unwrap_or_default()) } +// Dart does not support changing environment variables. +// `Platform.environment['MY_VAR'] = 'VAR';` will throw an error +// `Unsupported operation: Cannot modify unmodifiable map`. +// +// And we need to share the environment variables between rust and dart isolates sometimes. +pub fn main_set_env(key: String, value: Option) -> SyncReturn<()> { + let is_valid_key = !key.is_empty() && !key.contains('=') && !key.contains('\0'); + debug_assert!(is_valid_key, "Invalid environment variable key: {}", key); + if !is_valid_key { + log::error!("Invalid environment variable key: {}", key); + return SyncReturn(()); + } + + match value { + Some(v) => { + let is_valid_value = !v.contains('\0'); + debug_assert!(is_valid_value, "Invalid environment variable value: {}", v); + if !is_valid_value { + log::error!("Invalid environment variable value: {}", v); + return SyncReturn(()); + } + std::env::set_var(key, v); + } + None => std::env::remove_var(key), + } + + SyncReturn(()) +} + pub fn main_set_local_option(key: String, value: String) { let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER); let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 38e2123776b..0560b1fb56b 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "تمكين الطرفية"), ("New tab", "تبويب جديد"), ("Keep terminal sessions on disconnect", "الاحتفاظ بجلسات الطرفية عند قطع الاتصال"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 85c424cae80..d8973788c72 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 1bf1a48456d..9863ac753da 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index c6ece9ec168..9b0c94da342 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 18915464b2a..01b4f8ed8e7 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "启用终端"), ("New tab", "新建选项卡"), ("Keep terminal sessions on disconnect", "断开连接时保持终端会话"), + ("Terminal (Run as administrator)", "终端(以管理员身份运行)"), + ("terminal-admin-login-tip", "请输入被控端的管理员账号密码。"), + ("Failed to get user token.", "获取用户令牌时出错。"), + ("Incorrect username or password.", "用户名或密码不正确。"), + ("The user is not an administrator.", "用户不是管理员。"), + ("Failed to check if the user is an administrator.", "检查用户是否为管理员时出错。"), + ("Supported only by the installation version.", "仅安装版本支持。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 1b5a0b49230..6faeed3c384 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 0aab88c9c02..24ecd2eb897 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 54f32be6309..73ea1787799 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Terminal zulassen"), ("New tab", "Neuer Tab"), ("Keep terminal sessions on disconnect", "Terminalsitzungen beim Trennen der Verbindung beibehalten"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 2accbd9a7b0..c96e3f3af3a 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 4570c832422..14904f076be 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -256,5 +256,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("download-new-version-failed-tip", "Download failed. You can try again or click the \"Download\" button to download from the release page and upgrade manually."), ("update-failed-check-msi-tip", "Installation method check failed. Please click the \"Download\" button to download from the release page and upgrade manually."), ("websocket_tip", "When using WebSocket, only relay connections are supported."), + ("terminal-admin-login-tip", "Please input the administrator username and password of the controlled side."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index a9dda55e73d..b64926a7213 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ac588e7106c..7ef10e4b593 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index bde37460148..bf671383388 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 46f3e8a9b78..4309202db0a 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 1836c2742e0..9cd27927c3b 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "فعال‌سازی ترمینال"), ("New tab", "زبانه جدید"), ("Keep terminal sessions on disconnect", "حفظ جلسات ترمینال پس از قطع اتصال"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 5e1266fd83e..7fbe290e30c 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Activer le terminal"), ("New tab", "Nouvel onglet"), ("Keep terminal sessions on disconnect", "Maintenir les sessions du terminal lors de la déconnexion"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 6adc2606d2b..f9fb90d06ab 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index af33c8c5fa6..c8254a3249e 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index e1ea1837f35..7063d3bdaee 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index df3044c6d37..e88a4f59ced 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Terminál engedélyezése"), ("New tab", "Új lap"), ("Keep terminal sessions on disconnect", "Terminál munkamenetek megtartása leválasztáskor"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index fcc72431df8..dde9c5d2516 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 96974e36cf3..c9ebbcd8747 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Abilita terminale"), ("New tab", "Nuova scheda"), ("Keep terminal sessions on disconnect", "Quando disconetti mantieni attiva sessione terminale"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 14e322eeba5..534b448e2f1 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 0efc42bdb6c..1ad948d5ea5 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "터미널 사용함"), ("New tab", "새 탭"), ("Keep terminal sessions on disconnect", "터미널 세션 연결 해제 상태 유지"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index b02bd1c0db0..e0377af5123 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 598a54efd6d..963e8d48d5c 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 9ef5c38f04e..d3f04a74acb 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Iespējot termināli"), ("New tab", "Jauna cilne"), ("Keep terminal sessions on disconnect", "Atvienojoties saglabāt termināļa sesijas"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index c2719c86e67..40c7512837f 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 0d4f6c808ca..8eef85b5337 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Terminal inschakelen"), ("New tab", "Nieuw tabblad"), ("Keep terminal sessions on disconnect", "Terminalsessies bij verbreking van de verbinding behouden"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 93b328f0a63..1f5432fb060 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index c4fc7818793..342656a65d6 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 1a715121d30..e8ed440ac65 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 41d86baab25..adbc5c24ff1 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 0b40df0a7e4..cea299b1566 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Включить терминал"), ("New tab", "Новая вкладка"), ("Keep terminal sessions on disconnect", "Сохранять сеансы терминала при отключении"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 14d5b7e0472..5405e8d42b3 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 439c90d0059..25abf15d515 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 78551d37d33..b4ff93c548b 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index f8aa4e6b32c..60bca13c563 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index f2be9629f6e..5b7cde1ac3a 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index ecd6122c576..a1504746326 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 27276683140..6ae5d833fca 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index a4934e82e6f..c9ff2f20edd 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 65774bffc55..9d0c4680992 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 8284dcc5107..2fbcf78f9cc 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index b5e44d07aaa..bd23f37098c 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 7a5103ef3c9..a8aa6bd4269 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 332c51d68f8..10690ef2750 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 45c5fc7abfa..a00e9906bfc 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -40,7 +40,7 @@ use winapi::{ shared::{minwindef::*, ntdef::NULL, windef::*, winerror::*}, um::{ errhandlingapi::GetLastError, - handleapi::CloseHandle, + handleapi::{CloseHandle, INVALID_HANDLE_VALUE}, libloaderapi::{ GetProcAddress, LoadLibraryA, LoadLibraryExA, LOAD_LIBRARY_SEARCH_SYSTEM32, }, @@ -49,15 +49,19 @@ use winapi::{ GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, OpenProcessToken, ProcessIdToSessionId, PROCESS_INFORMATION, STARTUPINFOW, }, - securitybaseapi::GetTokenInformation, + securitybaseapi::{ + AllocateAndInitializeSid, DuplicateToken, EqualSid, FreeSid, GetTokenInformation, + }, shellapi::ShellExecuteW, sysinfoapi::{GetNativeSystemInfo, SYSTEM_INFO}, winbase::*, wingdi::*, winnt::{ - TokenElevation, ES_AWAYMODE_REQUIRED, ES_CONTINUOUS, ES_DISPLAY_REQUIRED, + SecurityImpersonation, TokenElevation, TokenGroups, TokenImpersonation, TokenType, + DOMAIN_ALIAS_RID_ADMINS, ES_AWAYMODE_REQUIRED, ES_CONTINUOUS, ES_DISPLAY_REQUIRED, ES_SYSTEM_REQUIRED, HANDLE, PROCESS_ALL_ACCESS, PROCESS_QUERY_LIMITED_INFORMATION, - TOKEN_ELEVATION, TOKEN_QUERY, + PSID, SECURITY_BUILTIN_DOMAIN_RID, SECURITY_NT_AUTHORITY, SID_IDENTIFIER_AUTHORITY, + TOKEN_ELEVATION, TOKEN_GROUPS, TOKEN_QUERY, TOKEN_TYPE, }, winreg::HKEY_CURRENT_USER, winspool::{ @@ -521,6 +525,10 @@ extern "C" { fn is_service_running_w(svc_name: *const u16) -> bool; } +pub fn get_current_session_id(share_rdp: bool) -> DWORD { + unsafe { get_current_session(if share_rdp { TRUE } else { FALSE }) } +} + extern "system" { fn BlockInput(v: BOOL) -> BOOL; } @@ -2158,6 +2166,177 @@ pub fn send_message_to_hnwd( return true; } +pub fn get_logon_user_token(user: &str, pwd: &str) -> ResultType { + let user_split = user.split("\\").collect::>(); + let wuser = wide_string(user_split.get(1).unwrap_or(&user)); + let wpc = wide_string(user_split.get(0).unwrap_or(&"")); + let wpwd = wide_string(pwd); + let mut ph_token: HANDLE = std::ptr::null_mut(); + let res = unsafe { + LogonUserW( + wuser.as_ptr(), + wpc.as_ptr(), + wpwd.as_ptr(), + LOGON32_LOGON_INTERACTIVE, + LOGON32_PROVIDER_DEFAULT, + &mut ph_token as _, + ) + }; + if res == FALSE { + bail!( + "Failed to log on user {}: {}", + user, + std::io::Error::last_os_error() + ); + } else { + if ph_token.is_null() { + bail!( + "Failed to log on user {}: {}", + user, + std::io::Error::last_os_error() + ); + } + Ok(ph_token) + } +} + +// Ensure the token returned is a primary token. +// If the provided token is an impersonation token, it duplicates it to a primary token. +// If the provided token is already a primary token, it returns it as is. +// The caller is responsible for closing the returned token handle. +pub fn ensure_primary_token(user_token: HANDLE) -> ResultType { + if user_token.is_null() || user_token == INVALID_HANDLE_VALUE { + bail!("Invalid user token provided"); + } + + unsafe { + let mut token_type: TOKEN_TYPE = 0; + let mut return_length: DWORD = 0; + + if GetTokenInformation( + user_token, + TokenType, + &mut token_type as *mut _ as *mut _, + std::mem::size_of::() as DWORD, + &mut return_length, + ) == FALSE + { + bail!( + "Failed to get token type, error {}", + io::Error::last_os_error() + ); + } + + if token_type == TokenImpersonation { + let mut duplicate_token: HANDLE = std::ptr::null_mut(); + let dup_res = DuplicateToken(user_token, SecurityImpersonation, &mut duplicate_token); + CloseHandle(user_token); + if dup_res == FALSE { + bail!( + "Failed to duplicate token, error {}", + io::Error::last_os_error() + ); + } + Ok(duplicate_token) + } else { + Ok(user_token) + } + } +} + +pub fn is_user_token_admin(user_token: HANDLE) -> ResultType { + if user_token.is_null() || user_token == INVALID_HANDLE_VALUE { + bail!("Invalid user token provided"); + } + + unsafe { + let mut dw_size: DWORD = 0; + GetTokenInformation( + user_token, + TokenGroups, + std::ptr::null_mut(), + 0, + &mut dw_size, + ); + + let last_error = GetLastError(); + if last_error != ERROR_INSUFFICIENT_BUFFER { + bail!( + "Failed to get token groups buffer size, error: {}", + last_error + ); + } + if dw_size == 0 { + bail!("Token groups buffer size is zero"); + } + + let mut buffer = vec![0u8; dw_size as usize]; + if GetTokenInformation( + user_token, + TokenGroups, + buffer.as_mut_ptr() as *mut _, + dw_size, + &mut dw_size, + ) == FALSE + { + bail!( + "Failed to get token groups information, error: {}", + io::Error::last_os_error() + ); + } + + let p_token_groups = buffer.as_ptr() as *const TOKEN_GROUPS; + let group_count = (*p_token_groups).GroupCount; + + if group_count == 0 { + return Ok(false); + } + + let mut nt_authority: SID_IDENTIFIER_AUTHORITY = SID_IDENTIFIER_AUTHORITY { + Value: SECURITY_NT_AUTHORITY, + }; + let mut administrators_group: PSID = std::ptr::null_mut(); + if AllocateAndInitializeSid( + &mut nt_authority, + 2, + SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_ADMINS, + 0, + 0, + 0, + 0, + 0, + 0, + &mut administrators_group, + ) == FALSE + { + bail!( + "Failed to allocate administrators group SID, error: {}", + io::Error::last_os_error() + ); + } + if administrators_group.is_null() { + bail!("Failed to create administrators group SID"); + } + + let mut is_admin = false; + let groups = + std::slice::from_raw_parts((*p_token_groups).Groups.as_ptr(), group_count as usize); + for group in groups { + if EqualSid(administrators_group, group.Sid) == TRUE { + is_admin = true; + break; + } + } + + if !administrators_group.is_null() { + FreeSid(administrators_group); + } + + Ok(is_admin) + } +} + pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> ResultType<()> { let last_error_table = HashMap::from([ ( diff --git a/src/server/connection.rs b/src/server/connection.rs index 12a3061de0a..9daf24f7848 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -56,6 +56,8 @@ use std::{ }; #[cfg(not(any(target_os = "android", target_os = "ios")))] use system_shutdown; +#[cfg(target_os = "windows")] +use windows::Win32::Foundation::{CloseHandle, HANDLE}; #[cfg(windows)] use crate::virtual_display_manager; @@ -172,6 +174,22 @@ pub enum AuthConnType { Terminal, } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[derive(Clone, Debug)] +enum TerminalUserToken { + SelfUser, + CurrentLogonUser(crate::terminal_service::UserToken), +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl TerminalUserToken { + fn to_terminal_service_token(&self) -> Option { + match self { + TerminalUserToken::SelfUser => None, + TerminalUserToken::CurrentLogonUser(token) => Some(*token), + } + } +} pub struct Connection { inner: ConnInner, display_idx: usize, @@ -254,6 +272,11 @@ pub struct Connection { tx_post_seq: mpsc::UnboundedSender<(String, Value)>, terminal_service_id: String, terminal_persistent: bool, + // The user token must be set when terminal is enabled. + // 0 indicates SYSTEM user + // other values indicate current user + #[cfg(not(any(target_os = "android", target_os = "ios")))] + terminal_user_token: Option, terminal_generic_service: Option>, } @@ -418,6 +441,8 @@ impl Connection { tx_post_seq, terminal_service_id: "".to_owned(), terminal_persistent: false, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + terminal_user_token: None, terminal_generic_service: None, }; let addr = hbb_common::try_into_v4(addr); @@ -1415,12 +1440,19 @@ impl Connection { .unwrap() .insert(self.lr.my_id.clone(), self.tx_input.clone()); + // Terminal feature is supported on desktop only + #[allow(unused_mut)] + let mut terminal = cfg!(not(any(target_os = "android", target_os = "ios"))); + #[cfg(target_os = "windows")] + { + terminal = terminal && portable_pty::win::check_support().is_ok(); + } pi.username = username; pi.sas_enabled = sas_enabled; pi.features = Some(Features { privacy_mode: privacy_mode::is_privacy_mode_supported(), #[cfg(not(any(target_os = "android", target_os = "ios")))] - terminal: true, // Terminal feature is supported on desktop only + terminal, ..Default::default() }) .into(); @@ -1429,7 +1461,9 @@ impl Connection { #[allow(unused_mut)] let mut wait_session_id_confirm = false; #[cfg(windows)] - self.handle_windows_specific_session(&mut pi, &mut wait_session_id_confirm); + if !self.terminal { + self.handle_windows_specific_session(&mut pi, &mut wait_session_id_confirm); + } if self.file_transfer.is_some() || self.terminal { res.set_peer_info(pi); } else if self.view_camera { @@ -1933,12 +1967,28 @@ impl Connection { sleep(1.).await; return false; } + #[cfg(target_os = "windows")] + if !lr.os_login.username.is_empty() && !crate::platform::is_installed() { + self.send_login_error("Supported only by the installation version.") + .await; + sleep(1.).await; + return false; + } + self.terminal = true; if let Some(o) = self.options_in_login.as_ref() { self.terminal_persistent = o.terminal_persistent.enum_value() == Ok(BoolOption::Yes); } self.terminal_service_id = terminal.service_id; + #[cfg(target_os = "windows")] + if let Some(msg) = + self.fill_terminal_user_token(&lr.os_login.username, &lr.os_login.password) + { + self.send_login_error(msg).await; + sleep(1.).await; + return false; + } } Some(login_request::Union::PortForward(mut pf)) => { if !Connection::permission("enable-tunnel") { @@ -2893,6 +2943,94 @@ impl Connection { true } + // Try to fill user token for terminal connection. + // If username is empty, use the user token of the current session. + // If username is not empty, try to logon and check if the user is an administrator. + // If the user is an administrator, use the user token of current process (SYSTEM). + // If the user is not an administrator, return an error message. + // Note: Only local and domain users are supported, Microsoft account (online account) not supported for now. + #[cfg(target_os = "windows")] + fn fill_terminal_user_token(&mut self, username: &str, password: &str) -> Option<&'static str> { + // No need to check if the password is empty. + if !username.is_empty() { + return self.handle_administrator_check(username, password); + } + + if crate::platform::is_prelogin() { + self.terminal_user_token = None; + return Some("No active console user logged on, please connect and logon first."); + } + + if crate::platform::is_installed() { + return self.handle_installed_user(); + } + + self.terminal_user_token = Some(TerminalUserToken::SelfUser); + None + } + + #[cfg(target_os = "windows")] + fn handle_administrator_check( + &mut self, + username: &str, + password: &str, + ) -> Option<&'static str> { + let check_admin_res = + crate::platform::get_logon_user_token(username, password).map(|token| { + let is_token_admin = crate::platform::is_user_token_admin(token); + unsafe { + hbb_common::allow_err!(CloseHandle(HANDLE(token as _))); + }; + is_token_admin + }); + match check_admin_res { + Ok(Ok(b)) => { + if b { + self.terminal_user_token = Some(TerminalUserToken::SelfUser); + None + } else { + Some("The user is not an administrator.") + } + } + Ok(Err(e)) => { + log::error!("Failed to check if the user is an administrator: {}", e); + Some("Failed to check if the user is an administrator.") + } + Err(e) => { + log::error!("Failed to get logon user token: {}", e); + Some("Incorrect username or password.") + } + } + } + + #[cfg(target_os = "windows")] + fn handle_installed_user(&mut self) -> Option<&'static str> { + let session_id = crate::platform::get_current_session_id(true); + if session_id == 0xFFFFFFFF { + return Some("Failed to get current session id."); + } + let token = crate::platform::get_user_token(session_id, true); + if !token.is_null() { + match crate::platform::ensure_primary_token(token) { + Ok(t) => { + self.terminal_user_token = Some(TerminalUserToken::CurrentLogonUser(t as _)); + } + Err(e) => { + log::error!("Failed to ensure primary token: {}", e); + self.terminal_user_token = + Some(TerminalUserToken::CurrentLogonUser(token as _)); + } + } + None + } else { + log::error!( + "Failed to get user token for terminal action, {}", + std::io::Error::last_os_error() + ); + Some("Failed to get user token.") + } + } + fn update_failure(&self, (mut failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) { if remove { if failure.0 != 0 { @@ -3833,12 +3971,19 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] async fn init_terminal_service(&mut self) { + debug_assert!(self.terminal_user_token.is_some()); + let Some(user_token) = self.terminal_user_token.clone() else { + // unreachable, but keep it for safety + log::error!("Terminal user token is not set."); + return; + }; if self.terminal_service_id.is_empty() { self.terminal_service_id = terminal_service::generate_service_id(); } let s = Box::new(terminal_service::new( self.terminal_service_id.clone(), self.terminal_persistent, + user_token.to_terminal_service_token(), )); s.on_subscribe(self.inner.clone()); self.terminal_generic_service = Some(s); @@ -3846,9 +3991,15 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] async fn handle_terminal_action(&mut self, action: TerminalAction) -> ResultType<()> { + debug_assert!(self.terminal_user_token.is_some()); + let Some(user_token) = self.terminal_user_token.clone() else { + // unreacheable, but keep it for safety + bail!("Terminal user token is not set."); + }; let mut proxy = terminal_service::TerminalServiceProxy::new( self.terminal_service_id.clone(), Some(self.terminal_persistent), + user_token.to_terminal_service_token(), ); match proxy.handle_action(&action) { @@ -4249,6 +4400,15 @@ impl Drop for Connection { if let Some(s) = self.terminal_generic_service.as_ref() { s.join(); } + + #[cfg(target_os = "windows")] + if let Some(TerminalUserToken::CurrentLogonUser(token)) = self.terminal_user_token.take() { + if token != 0 { + unsafe { + hbb_common::allow_err!(CloseHandle(HANDLE(token as _))); + }; + } + } } } diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index d709454c941..23340e5e91b 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -7,6 +7,7 @@ use portable_pty::{Child, CommandBuilder, PtySize}; use std::{ collections::{HashMap, VecDeque}, io::{Read, Write}, + ops::{Deref, DerefMut}, sync::{ atomic::{AtomicBool, Ordering}, mpsc::{self, Receiver, SyncSender}, @@ -271,17 +272,51 @@ pub fn get_terminal_session_count(include_zombie_tasks: bool) -> usize { c } -pub fn new(service_id: String, is_persistent: bool) -> GenericService { +pub type UserToken = u64; + +#[derive(Clone)] +pub struct TerminalService { + sp: GenericService, + user_token: Option, +} + +impl Deref for TerminalService { + type Target = ServiceTmpl; + + fn deref(&self) -> &Self::Target { + &self.sp + } +} + +impl DerefMut for TerminalService { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.sp + } +} + +pub fn get_service_name(source: VideoSource, idx: usize) -> String { + format!("{}{}", source.service_name_prefix(), idx) +} + +pub fn new( + service_id: String, + is_persistent: bool, + user_token: Option, +) -> GenericService { // Create the service with initial persistence setting allow_err!(get_or_create_service(service_id.clone(), is_persistent)); - let svc = EmptyExtraFieldService::new(service_id.clone(), false); + let svc = TerminalService { + sp: GenericService::new(service_id.clone(), false), + user_token, + }; GenericService::run(&svc.clone(), move |sp| run(sp, service_id.clone())); svc.sp } -fn run(sp: EmptyExtraFieldService, service_id: String) -> ResultType<()> { +fn run(sp: TerminalService, service_id: String) -> ResultType<()> { while sp.ok() { - let responses = TerminalServiceProxy::new(service_id.clone(), None).read_outputs(); + let responses = TerminalServiceProxy::new(service_id.clone(), None, sp.user_token.clone()) + .read_outputs(); for response in responses { let mut msg_out = Message::new(); msg_out.set_terminal_response(response); @@ -451,6 +486,7 @@ impl TerminalSession { } drop(input_tx); } + self.output_rx = None; // Wait for threads to finish // The reader thread should join before the writer thread on Windows. @@ -544,6 +580,8 @@ impl PersistentTerminalService { pub struct TerminalServiceProxy { service_id: String, is_persistent: bool, + #[cfg(target_os = "windows")] + user_token: Option, } pub fn set_persistent(service_id: &str, is_persistent: bool) -> Result<()> { @@ -556,7 +594,11 @@ pub fn set_persistent(service_id: &str, is_persistent: bool) -> Result<()> { } impl TerminalServiceProxy { - pub fn new(service_id: String, is_persistent: Option) -> Self { + pub fn new( + service_id: String, + is_persistent: Option, + _user_token: Option, + ) -> Self { // Get persistence from the service if it exists let is_persistent = is_persistent.unwrap_or(if let Some(service) = get_service(&service_id) { @@ -567,6 +609,8 @@ impl TerminalServiceProxy { TerminalServiceProxy { service_id, is_persistent, + #[cfg(target_os = "windows")] + user_token: _user_token, } } @@ -670,7 +714,14 @@ impl TerminalServiceProxy { // Use default shell for the platform let shell = get_default_shell(); log::debug!("Using shell: {}", shell); - let cmd = CommandBuilder::new(&shell); + + #[allow(unused_mut)] + let mut cmd = CommandBuilder::new(&shell); + + #[cfg(target_os = "windows")] + if let Some(token) = &self.user_token { + cmd.set_user_token(*token as _); + } log::debug!("Spawning shell process..."); let child = pty_pair From 69af5f2fa609ce8cdccb39147056cb911ac1a4c0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 15 Jul 2025 18:49:45 +0800 Subject: [PATCH 404/506] update hwcodec (#12303) * Test necessary codecs in single thread * Terminate test process with parent process Signed-off-by: 21pages --- Cargo.lock | 2 +- libs/scrap/src/common/hwcodec.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index d73b36b09fe..aabff852e0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3288,7 +3288,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.1" -source = "git+https://github.com/rustdesk-org/hwcodec#0ea7e709d3c48bb6446e33a9cc8fd0e0da5709b9" +source = "git+https://github.com/rustdesk-org/hwcodec#17c1dbb38450fe4a64aeba78fb50bec32f364a16" dependencies = [ "bindgen 0.59.2", "cc", diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 7ee9b3d61d5..8f3cd6d0c71 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -678,6 +678,8 @@ impl HwCodecConfig { } pub fn check_available_hwcodec() -> String { + #[cfg(any(target_os = "linux", target_os = "macos"))] + hwcodec::common::setup_parent_death_signal(); let ctx = EncodeContext { name: String::from(""), mc_name: None, @@ -724,6 +726,8 @@ pub fn start_check_process() { if let Some(_) = exe.file_name().to_owned() { let arg = "--check-hwcodec-config"; if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() { + #[cfg(windows)] + hwcodec::common::child_exit_when_parent_exit(child.id()); // wait up to 30 seconds, it maybe slow on windows startup for poorly performing machines for _ in 0..30 { std::thread::sleep(std::time::Duration::from_secs(1)); From 65c721e088717b5efea786a1697ac97d032e0537 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:09:04 +0800 Subject: [PATCH 405/506] fix: terminal connection on Linux and MacOS (#12307) Signed-off-by: fufesou --- src/server/connection.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 9daf24f7848..eedb77995dc 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1981,7 +1981,7 @@ impl Connection { o.terminal_persistent.enum_value() == Ok(BoolOption::Yes); } self.terminal_service_id = terminal.service_id; - #[cfg(target_os = "windows")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Some(msg) = self.fill_terminal_user_token(&lr.os_login.username, &lr.os_login.password) { @@ -2943,6 +2943,12 @@ impl Connection { true } + #[cfg(any(target_os = "linux", target_os = "macos"))] + fn fill_terminal_user_token(&mut self, _username: &str, _password: &str) -> Option<&'static str> { + self.terminal_user_token = Some(TerminalUserToken::SelfUser); + None + } + // Try to fill user token for terminal connection. // If username is empty, use the user token of the current session. // If username is not empty, try to logon and check if the user is an administrator. From d5eb87ee8ba0e0ab08d22639ba96af6f09b78344 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:36:16 +0800 Subject: [PATCH 406/506] fix: try to fix stuck on read (#12310) Signed-off-by: fufesou --- src/server/terminal_service.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index 23340e5e91b..9f389502b60 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -481,6 +481,7 @@ impl TerminalSession { if let Some(input_tx) = self.input_tx.take() { // Send a final newline to ensure the reader can read some data, and then exit. // This is required on Windows and Linux. + // Although `self.pty_pair = None;` is called below, we can still send a final newline here. if let Err(e) = input_tx.send(b"\r\n".to_vec()) { log::warn!("Failed to send final newline to the terminal: {}", e); } @@ -488,6 +489,20 @@ impl TerminalSession { } self.output_rx = None; + // 1. Windows + // `pty_pair` uses pipe. https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/win/conpty.rs#L16 + // `read()` may stuck at https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/filedescriptor/src/windows.rs#L345 + // We can close the pipe to signal the reader thread to exit. + // After https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/win/psuedocon.rs#L86, the reader reads `[27, 91, 63, 57, 48, 48, 49, 108, 27, 91, 63, 49, 48, 48, 52, 108]` in my tests. + // 2. Linux + // `pty_pair` uses `libc::openpty`. https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/unix.rs#L32 + // We can also call the drop method first. https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/unix.rs#L352 + // The reader will get [13, 10] after dropping the `pty_pair`. + // 3. macOS + // No stuck cases have been found so far, more testing is needed. + #[cfg(any(target_os = "windows", target_os = "linux"))] + self.pty_pair = None; + // Wait for threads to finish // The reader thread should join before the writer thread on Windows. if let Some(reader_thread) = self.reader_thread.take() { From e31b04b6a7dd0a914badb33ee16671f19c8441eb Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 16 Jul 2025 09:25:47 +0800 Subject: [PATCH 407/506] fix: new translation message (#12312) Signed-off-by: fufesou --- 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/ge.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/sc.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/ta.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/vi.rs | 2 +- src/server/connection.rs | 2 +- 47 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 0560b1fb56b..66fbb953385 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index d8973788c72..22a9c3ced8a 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 9863ac753da..f4d71f280d2 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 9b0c94da342..125b1c77db0 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 01b4f8ed8e7..2d4e0ee2cc5 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", "用户名或密码不正确。"), ("The user is not an administrator.", "用户不是管理员。"), ("Failed to check if the user is an administrator.", "检查用户是否为管理员时出错。"), - ("Supported only by the installation version.", "仅安装版本支持。"), + ("Supported only in the installed version.", "仅在以安装版本受支持。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 6faeed3c384..dfe66bb5bfe 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 24ecd2eb897..c09f54260c6 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 73ea1787799..5d92a7a870b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index c96e3f3af3a..0404661a54b 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index b64926a7213..f6ddf2fcfbe 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 7ef10e4b593..9f93854be27 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index bf671383388..b5ba0892861 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 4309202db0a..1ad0466f986 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 9cd27927c3b..18eeded8e53 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 7fbe290e30c..ea6dea5459e 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index f9fb90d06ab..2f227bbf9cb 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index c8254a3249e..20f0daf4a36 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 7063d3bdaee..9f1aeecabb6 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index e88a4f59ced..974f85831fe 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index dde9c5d2516..75d69f777ae 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index c9ebbcd8747..62907991463 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 534b448e2f1..d6693564f2d 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 1ad948d5ea5..e9b034eb794 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index e0377af5123..b7edc45650f 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 963e8d48d5c..4911c75dec1 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index d3f04a74acb..2685590576b 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 40c7512837f..d10c671dd77 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 8eef85b5337..ec2cb387190 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 1f5432fb060..619a8910039 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 342656a65d6..f8fe45d34e3 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index e8ed440ac65..5257dd7f427 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index adbc5c24ff1..b1fd7340d26 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index cea299b1566..cf51cbaa73d 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 5405e8d42b3..8067c66acd4 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 25abf15d515..6a093d92473 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index b4ff93c548b..59236932212 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 60bca13c563..f01b913f670 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 5b7cde1ac3a..20fb6f5ee59 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index a1504746326..fe8589b565f 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 6ae5d833fca..266dcf94f73 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index c9ff2f20edd..5302d208c04 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 9d0c4680992..919a6d54cda 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 2fbcf78f9cc..19b7a4c60f9 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index bd23f37098c..55da035bb0d 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index a8aa6bd4269..b0037857819 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 10690ef2750..44707f64bca 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incorrect username or password.", ""), ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), - ("Supported only by the installation version.", ""), + ("Supported only in the installed version.", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index eedb77995dc..7da62950826 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1969,7 +1969,7 @@ impl Connection { } #[cfg(target_os = "windows")] if !lr.os_login.username.is_empty() && !crate::platform::is_installed() { - self.send_login_error("Supported only by the installation version.") + self.send_login_error("Supported only in the installed version.") .await; sleep(1.).await; return false; From 661be6ae3630b982db83c9e7ccb9be5e4d7e7dd8 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 16 Jul 2025 09:28:24 +0800 Subject: [PATCH 408/506] fix: build (#12313) Signed-off-by: fufesou --- src/server/terminal_service.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index 9f389502b60..e369de8f863 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -501,7 +501,9 @@ impl TerminalSession { // 3. macOS // No stuck cases have been found so far, more testing is needed. #[cfg(any(target_os = "windows", target_os = "linux"))] - self.pty_pair = None; + { + self.pty_pair = None; + } // Wait for threads to finish // The reader thread should join before the writer thread on Windows. From e711f73451480e234e9ca915508e59b2b06284d1 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 16 Jul 2025 14:17:16 +0800 Subject: [PATCH 409/506] fix: macos, defunct process (#12315) Signed-off-by: fufesou --- src/server.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/server.rs b/src/server.rs index 0dcaf7e414f..39d0add862b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -231,11 +231,13 @@ pub async fn create_tcp_connection( #[cfg(target_os = "macos")] { use std::process::Command; - Command::new("/usr/bin/caffeinate") + if let Ok(task) = Command::new("/usr/bin/caffeinate") .arg("-u") .arg("-t 5") .spawn() - .ok(); + { + super::CHILD_PROCESS.lock().unwrap().push(task); + } log::info!("wake up macos"); } Connection::start(addr, stream, id, Arc::downgrade(&server)).await; From 475bef63d7114955ec6c707d0a33663c630dae58 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 17 Jul 2025 08:46:32 +0800 Subject: [PATCH 410/506] fix: linux, env TERM (#12325) Signed-off-by: fufesou --- Cargo.lock | 81 ++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 1 + src/platform/linux.rs | 82 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 156 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aabff852e0a..a5eee545ed3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1790,6 +1790,15 @@ dependencies = [ "dirs-sys 0.3.7", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "5.0.1" @@ -5090,7 +5099,16 @@ version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" dependencies = [ - "phf_shared", + "phf_shared 0.7.24", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", ] [[package]] @@ -5099,8 +5117,18 @@ version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.7.24", + "phf_shared 0.7.24", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", ] [[package]] @@ -5109,17 +5137,36 @@ version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" dependencies = [ - "phf_shared", + "phf_shared 0.7.24", "rand 0.6.5", ] +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + [[package]] name = "phf_shared" version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" dependencies = [ - "siphasher", + "siphasher 0.2.3", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", ] [[package]] @@ -6146,6 +6193,7 @@ dependencies = [ "system_shutdown", "tao", "tauri-winrt-notification", + "terminfo", "termios 0.3.3", "totp-rs", "tray-icon", @@ -6674,6 +6722,12 @@ 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" @@ -7033,8 +7087,8 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "013d134ae4a25ee744ad6129db589018558f620ddfa44043887cdd45fa08e75c" dependencies = [ - "phf", - "phf_codegen", + "phf 0.7.24", + "phf_codegen 0.7.24", "serde_json 0.9.10", ] @@ -7069,6 +7123,19 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminfo" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f" +dependencies = [ + "dirs 4.0.0", + "fnv", + "nom", + "phf 0.11.3", + "phf_codegen 0.11.3", +] + [[package]] name = "termios" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index da8c3bff0e0..7eb796d8600 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -180,6 +180,7 @@ once_cell = {version = "1.18", optional = true} nix = { version = "0.29", features = ["term", "process"]} gtk = "0.18" termios = "0.3" +terminfo = "0.8" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.13" diff --git a/src/platform/linux.rs b/src/platform/linux.rs index f17c5b472fa..ec6210e2971 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -10,7 +10,6 @@ use hbb_common::{ libc::{c_char, c_int, c_long, c_void}, log, message_proto::{DisplayInfo, Resolution}, - platform::linux::{CMD_PS, CMD_SH}, regex::{Captures, Regex}, }; use std::{ @@ -25,6 +24,7 @@ use std::{ }, time::{Duration, Instant}, }; +use terminfo::{capability as cap, Database}; use users::{get_user_by_name, os::unix::UserExt}; use wallpaper; @@ -33,8 +33,20 @@ type Xdo = *const c_void; pub const PA_SAMPLE_RATE: u32 = 48000; static mut UNMODIFIED: bool = true; +const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"]; +const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"]; + lazy_static::lazy_static! { pub static ref IS_X11: bool = hbb_common::platform::linux::is_x11_or_headless(); + static ref DATABASE_XTERM_256COLOR: Option = { + match Database::from_name("xterm-256color") { + Ok(database) => Some(database), + Err(err) => { + log::error!("Failed to initialize xterm-256color database: {}", err); + None + } + } + }; } thread_local! { @@ -256,6 +268,70 @@ fn start_uinput_service() { }); } +/// Suggests the best terminal type based on the environment. +/// +/// The function prioritizes terminal types in the following order: +/// 1. `screen-256color`: Preferred when running inside `tmux` or `screen` sessions, +/// as these multiplexers often support advanced terminal features. +/// 2. `xterm-256color`: Selected if the terminal supports 256 colors, which is +/// suitable for modern terminal applications. +/// 3. `xterm`: Used as a fallback for basic terminal compatibility. +/// +/// Terminals like `linux` and `vt100` are excluded because they lack support for +/// modern features required by many applications. +fn suggest_best_term() -> String { + if is_running_in_tmux() || is_running_in_screen() { + return "screen-256color".to_string(); + } + if term_supports_256_colors("xterm-256color") { + return "xterm-256color".to_string(); + } + "xterm".to_string() +} + +fn is_running_in_tmux() -> bool { + std::env::var("TMUX").is_ok() +} + +fn is_running_in_screen() -> bool { + std::env::var("STY").is_ok() +} + +fn supports_256_colors(db: &Database) -> bool { + db.get::().map_or(false, |n| n.0 >= 256) +} + +fn term_supports_256_colors(term: &str) -> bool { + match term { + "xterm-256color" => DATABASE_XTERM_256COLOR + .as_ref() + .map_or(false, |db| supports_256_colors(db)), + _ => Database::from_name(term).map_or(false, |db| supports_256_colors(&db)), + } +} + +fn get_cur_term(uid: &str) -> Option { + if uid.is_empty() { + return None; + } + + if let Ok(term) = std::env::var("TERM") { + if !INVALID_TERM_VALUES.contains(&term.as_str()) { + return Some(term); + } + } + + for proc in SHELL_PROCESSES { + // Construct a regex pattern to match either the process name followed by '$' or 'bin/' followed by the process name. + let term = get_env("TERM", uid, &format!("{}$|bin/{}", proc, proc)); + if !INVALID_TERM_VALUES.contains(&term.as_str()) { + return Some(term); + } + } + + None +} + #[inline] fn try_start_server_(desktop: Option<&Desktop>) -> ResultType> { match desktop { @@ -273,6 +349,10 @@ fn try_start_server_(desktop: Option<&Desktop>) -> ResultType> { if !desktop.home.is_empty() { envs.push(("HOME", desktop.home.clone())); } + envs.push(( + "TERM", + get_cur_term(&desktop.uid).unwrap_or_else(|| suggest_best_term()), + )); run_as_user( vec!["--server"], Some((desktop.uid.clone(), desktop.username.clone())), From 4d960c3c8ce4a222a9486ad9c8f5209a5d5f03e3 Mon Sep 17 00:00:00 2001 From: WC3D <57880529+WC3D@users.noreply.github.com> Date: Wed, 16 Jul 2025 20:54:53 -0400 Subject: [PATCH 411/506] Potential fix for code scanning alert no. 29: Workflow does not contain permissions (#12326) If a GitHub Actions job or workflow has no explicit permissions set, then the repository permissions are used. Repositories created under an organization inherit the organization's permissions. Organizations or repositories created before February 2023 have default permissions set to read-write. Often, these permissions do not adhere to the principle of least privilege and can be reduced to read-only, leaving write permission only for specific types, such as issues (write) or pull requests (write). Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/flutter-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 36bbe790203..c028844f678 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -2084,6 +2084,8 @@ jobs: if: False name: build-rustdesk-web runs-on: ubuntu-22.04 + permissions: + contents: read strategy: fail-fast: false env: From effbb45eb7cd24bc0c2c2c3b6704780e76a9a1ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Thu, 17 Jul 2025 21:53:15 +0900 Subject: [PATCH 412/506] Update README-KR.md (#12301) Translation Update --- docs/README-KR.md | 70 +++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/docs/README-KR.md b/docs/README-KR.md index d3f25509bbf..b015b4a4dbf 100644 --- a/docs/README-KR.md +++ b/docs/README-KR.md @@ -1,65 +1,65 @@

    - RustDesk - Your remote desktop
    - Build • + RustDesk - Your remote desktop
    + 빌드Docker • - Structure • - Snapshot
    - [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] | [Norsk]
    - 이 README와 RustDesk UIRustDesk 문서를 여러분의 모국어로 번역하는 데 도움이 필요합니다. + 구조 • + 스크린샷
    + [Українська] | [č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]
    + 이 README, RustDesk UI and RustDesk 문서를 귀하의 모국어로 번역하는 데 도움이 필요합니다

    -> [!Caution] -> **오용 관련 면책 조항:**
    -> RustDesk 개발자는 이 소프트웨어의 비윤리적이거나 불법적인 사용을 용납하거나 지원하지 않습니다. 무단 액세스, 제어 또는 사생활 침해와 같은 오용은 당사의 가이드라인에 엄격히 위배됩니다. 개발자는 애플리케이션의 오용에 대해 책임을 지지 않습니다. +> [!주의] +> **오용 면책 조항:**
    +> RustDesk의 개발자는 이 소프트웨어의 비윤리적 또는 불법적인 사용을 묵인하거나 지원하지 않습니다. 무단 액세스, 제어 또는 개인정보 침해와 같은 오용은 엄격하게 당사의 지침에 위배됩니다. 작성자는 응용 프로그램의 오용에 대해 책임을 지지 않습니다. -채팅하기: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) +채팅: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@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 server 서버를 사용하거나, [직접 설정](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) -RustDesk는 모든 기여를 환영합니다. 기여하고 싶다면 [`CONTRIBUTING-KR.md`](CONTRIBUTING-KR.md)를 참고해 주세요. +RustDesk는 모든 분들의 기여를 환영합니다. 시작하는 데 도움이 필요하면. [CONTRIBUTING.md](docs/CONTRIBUTING.md)를 참조하세요.. -[**자주 묻는 질문 (FAQ)**](https://github.com/rustdesk/rustdesk/wiki/FAQ) +[**자주 묻는 질문**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**바이너리 다운로드**](https://github.com/rustdesk/rustdesk/releases) -[**나이틀리 빌드**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) +[**개발자 빌드**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) [F-Droid에서 다운로드](https://f-droid.org/en/packages/com.carriez.flutter_hbb) [Flathub에서 다운로드](https://flathub.org/apps/com.rustdesk.RustDesk) -## 의존성 +## 종속성 -데스크톱 버전은 GUI에 Flutter 또는 Sciter (지원 중단됨)를 사용합니다. 이 튜토리얼은 Sciter 전용이며, 시작하기 더 쉽고 친숙하기 때문입니다. Flutter 버전 빌드는 [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)를 확인하세요. +데스크톱 버전은 GUI로 Flutter 또는 Sciter (더 이상 지원되지 않음)를 사용하며, 이 튜토리얼은 시작하기 더 쉽고 친숙한 Sciter 전용입니다. Flutter 버전 빌드는 [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)을 확인하세요.. -Sciter 동적 라이브러리를 직접 다운로드하세요. +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) | [macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) -## 기본 빌드 방법 +## 빌드를 위한 원시 단계 -- Rust 개발 환경과 C++ 빌드 환경을 준비하세요. +- Rust 개발 환경과 C++ 빌드 환경을 준비합니다 -- [vcpkg](https://github.com/microsoft/vcpkg)를 설치하고 `VCPKG_ROOT` 환경변수를 정확히 설정하세요. +- [vcpkg](https://github.com/microsoft/vcpkg)를 설치하고 `VCPKG_ROOT` 환경 변수를 올바르게 설정합니다 - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static - - Linux/MacOS: vcpkg install libvpx libyuv opus aom + - Linux/macOS: vcpkg install libvpx libyuv opus aom -- `cargo run`을 실행합니다. +- `cargo run` 실행 ## [빌드](https://rustdesk.com/docs/en/dev/build/) -## Linux에서 빌드 방법 +## Linux에서 빌드하는 방법 ### Ubuntu 18 (Debian 10) @@ -99,7 +99,7 @@ export VCPKG_ROOT=$HOME/vcpkg vcpkg/vcpkg install libvpx libyuv opus aom ``` -### libvpx 수정 (For Fedora용) +### libvpx 수정 (Fedora용) ```sh cd vcpkg/buildtrees/libvpx/src @@ -136,41 +136,41 @@ git submodule update --init --recursive 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`를 붙여 실행합니다. 결과 실행 파일은 시스템의 target 폴더에 생성되며, 다음 명령으로 실행할 수 있습니다: +첫 번째 빌드는 종속성이 캐시되기까지 시간이 오래 걸릴 수 있으며, 이후 빌드는 더 빨라집니다. 또한 빌드 명령에 다른 인수를 지정해야 하는 경우 명령 끝의 `` 위치에 인수를 지정할 수 있습니다. 예를 들어 최적화된 릴리스 버전을 빌드하려면 위의 명령 뒤에 `--release`를 추가하면 됩니다. 결과 실행 파일은 시스템의 대상 폴더에서 사용할 수 있으며 실행할 수 있습니다:: ```sh target/debug/rustdesk ``` -또는, 릴리즈 실행 파일을 실행하는 경우: +또는 릴리스 실행 파일을 실행하는 경우: ```sh target/release/rustdesk ``` -이 명령들은 RustDesk 리포지토리의 루트 디렉토리에서 실행해야 합니다. 그렇지 않으면 애플리케이션이 필요한 리소스를 찾지 못할 수 있습니다. 또한, `install` 또는 `run`과 같은 cargo 하위 명령은 호스트가 아닌 컨테이너 내부에 프로그램을 설치하거나 실행하므로 현재 이 방식은 지원되지 않습니다. 이 점에 유의해 주세요. +RustDesk 리포지토리의 루트에서 이러한 명령을 실행하고 있는지 확인하세요. 그렇지 않으면 응용 프로그램이 필요한 리소스를 찾지 못할 수 있습니다. 또한 `install` 또는 `run` 과 같은 다른 cargo 하위 명령은 호스트가 아닌 컨테이너 내부에 프로그램을 설치하거나 실행하므로 현재 이 방법을 통해 지원되지 않는다는 점에 유의하세요. ## 파일 구조 -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 설정, TCP/UDP 래퍼, Protobuf, 파일 전송을 위한 fs 함수 및 기타 유틸리티 함수 -- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡처 +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 구성, tcp/udp wrapper, protobuf, 파일 전송을 위한 fs 함수 및 기타 유틸리티 함수 +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡쳐 - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 플랫폼별 키보드/마우스 제어 - **[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/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)와 통신, Remote Direct (TCP Hole Punching) 또는 Relayed Connection 대기 +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신, 원격 다이렉트 (TCP 홀 펀칭) 또는 릴레이 연결 대기 - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫폼별 코드 - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 데스크톱 및 모바일용 Flutter 코드 - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter 웹 클라이언트용 JavaScript -## 스냅샷 +## 스크린샷 ![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) From dc4149556607a9664433be3c03b3821f7994f482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Thu, 17 Jul 2025 21:58:21 +0900 Subject: [PATCH 413/506] Update CONTRIBUTING-KR.md (#12302) --- docs/CONTRIBUTING-KR.md | 52 +++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/docs/CONTRIBUTING-KR.md b/docs/CONTRIBUTING-KR.md index e0e4aa76653..5e432648eb2 100644 --- a/docs/CONTRIBUTING-KR.md +++ b/docs/CONTRIBUTING-KR.md @@ -1,40 +1,46 @@ -# RustDesk에 기여하기 +# RustDesk 기여하기 -RustDesk는 모든 분들의 기여를 환영합니다. RustDesk에 기여하고 싶으시다면 아래 가이드를 참고해 주세요: +RustDesk는 모든 분들의 참여를 환영합니다. 저희를 도와주실 생각이 있으시다면 + 다음 지침을 따르세요: -## 기여 방법 +## 기여 -RustDesk 프로젝트 또는 관련 라이브러리에 대한 기여는 GitHub 풀 리퀘스트(Pull Request) 형태로 이루어져야 합니다. -각 풀 리퀘스트는 핵심 기여자(패치 적용 권한이 있는 사람)가 검토하며, -메인 브랜치에 통합되거나 필요한 변경 사항에 대한 피드백을 받게 됩니다. -핵심 기여자를 포함한 모든 기여자는 이 형식을 따라야 합니다. +RustDesk 또는 그 종속성에 대한 기여는 GitHub 풀 리퀘스트 형태로 +이루어져야 합니다. 각 풀 리퀘스트는 핵심 기여자 (패치 적용 권한이 +있는 사람)가 검토하여 메인 트리에 추가하거나 필요한 변경 사항에 +대한 피드백을 제공합니다. 핵심 기여자의 기여를 포함하여 모든 기여는 +이 형식을 따라야 합니다. -특정 이슈에 대해 작업하고 싶다면, 먼저 해당 GitHub 이슈에 댓글을 달아 작업 의사를 알려주세요. -이는 여러 기여자가 동일한 이슈에 대해 중복으로 작업하는 것을 방지하기 위함입니다. +이슈에 대해 작업하고 싶으시면 먼저 해당 이슈에 대해 작업하고 싶다는 +댓글을 달아 해당 이슈를 요청하세요. 이는 동일한 이슈에 대한 기여자의 +중복된 노력을 방지하기 위한 것입니다. ## 풀 리퀘스트 체크리스트 -- master 브랜치에서 새 브랜치를 만들고, 필요한 경우 Pull Request를 제출하기 전에 현재 master - 브랜치로 리베이스하세요. master 브랜치와 깔끔하게 병합(merge)되지 않으면 변경 사항을 - 리베이스하도록 요청받을 수 있습니다. +- Master 브랜치에서 브랜치를 만들고, 필요한 경우 풀 리퀘스트를 제출하기 + 전에 현재 마스터 브랜치로 리베이스하세요. 마스터 브랜치와 깔끔하게 + 병합되지 않으면 변경 사항을 리베이스하라는 요청을 받을 수 있습니다. -- 커밋(commit)은 가능한 한 작게 유지하고, 각 커밋이 독립적으로 올바른지 (즉, 각 커밋이 컴파일되고 테스트를 통과하는지) 확인해야 합니다. +- 커밋은 가능한 한 작아야 하지만, 각 커밋이 독립적으로 올바른지 확인 + 해야 합니다 (즉, 각 커밋은 컴파일되어 테스트를 통과해야 함). -- 커밋에는 개발자 원본 증명서(DCO, Developer Certificate of Origin - http://developercertificate.org) 서명이 포함되어야 합니다. 이는 기여자(해당하는 경우 - 기여자의 고용주 포함)가 [프로젝트 라이선스](../LICENCE) 조건에 동의함을 의미합니다. - Git에서는 `git commit` 명령어에 `-s` 옵션을 사용합니다. +- 커밋에는 개발자 출처 증명서 (http://developercertificate.org) + 서명이 첨부되어야 하며, 이는 귀하 (및 해당되는 경우 고용주)가 + [프로젝트 라이선스](../LICENCE). 조건에 구속되는 데 동의한다는 것을 나타냅니다. + git에서는 `git commit`에 `-s` 옵션입니다 -- 패치가 검토되지 않거나 특정 리뷰어의 검토가 필요하다면, 풀 리퀘스트나 댓글에서 - @멘션으로 리뷰어에게 알리거나 [이메일](mailto:info@rustdesk.com)로 검토를 요청할 수 있습니다. +- 패치가 검토되지 않거나 특정인이 검토해야 하는 경우, 풀 리퀘스트나 + 댓글에서 검토자에게 @-답글을 보내 검토를 요청하거나 + [이메일](mailto:info@rustdesk.com)을 통해 검토를 요청할 수 있습니다. -- 수정한 버그나 추가한 기능과 관련된 테스트 코드를 포함해 주세요. +- 수정된 버그 또는 새 기능과 관련된 테스트를 추가합니다. -Git 사용에 대한 자세한 내용은 [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow) 문서를 참고하세요. +구체적인 git 지침은, [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow)을 참조하세요. -## 기여자 행동 강령 +## 행동 강령 https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md -## 소통 채널 +## 커뮤니케이션 -RustDesk 기여자들은 주로 [Discord](https://discord.gg/nDceKgxnkV)에서 소통합니다. +RustDesk 기여자들은 [Discord](https://discord.gg/nDceKgxnkV)에서 활동하고 있습니다. From 398b0d8d8b0162dbf03fa369191fc7ca5ce354f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Thu, 17 Jul 2025 22:11:58 +0900 Subject: [PATCH 414/506] Update SECURITY-KR.md (#12308) --- docs/SECURITY-KR.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/SECURITY-KR.md b/docs/SECURITY-KR.md index d1f576e3f18..94ce8f2ba18 100644 --- a/docs/SECURITY-KR.md +++ b/docs/SECURITY-KR.md @@ -2,6 +2,6 @@ ## 취약점 보고 -저희는 프로젝트의 보안을 매우 중요하게 생각합니다. 모든 사용자가 발견한 취약점을 저희에게 보고할 것을 권장합니다. RustDesk 프로젝트에서 보안 취약점이 발견되면 info@rustdesk.com 로 이메일을 보내 책임감 있게 보고해 주시기 바랍니다. +저희는 프로젝트의 보안을 매우 중요하게 생각합니다. 모든 사용자가 발견한 취약점을 저희에게 보고할 것을 권장합니다. RustDesk 프로젝트에서 보안 취약점이 발견되면 info@rustdesk.com으로 이메일을 보내 책임감 있게 보고해 주시기 바랍니다. -현재로서는 버그 현상금 프로그램이 없습니다. 저희는 큰 문제를 해결하기 위해 노력하는 소규모 팀입니다. 전체 커뮤니티를 위한 안전한 애플리케이션을 계속 구축할 수 있도록 취약점을 책임감 있게 신고해 주시기 바랍니다. +현재로서는 버그 현상금 프로그램이 없습니다. 저희는 큰 문제를 해결하기 위해 노력하는 소규모 팀입니다. 전체 커뮤니티를 위한 안전한 응용 프로그램을 계속 구축할 수 있도록 취약점을 책임감 있게 신고해 주시기 바랍니다. From bdd3bb946e94b967efa2796b1f5333e314b271a8 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:51:53 +0800 Subject: [PATCH 415/506] refact: restore terminals (#12334) Signed-off-by: fufesou --- flutter/lib/consts.dart | 1 + .../lib/desktop/pages/terminal_tab_page.dart | 41 ++++++++++++++++--- flutter/lib/models/terminal_model.dart | 14 +++++++ libs/hbb_common | 2 +- src/flutter.rs | 3 ++ src/server/terminal_service.rs | 23 +++++++++++ 6 files changed, 78 insertions(+), 6 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index ef42318f030..eda0e11cff4 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -64,6 +64,7 @@ const String kWindowEventNewFileTransfer = "new_file_transfer"; const String kWindowEventNewViewCamera = "new_view_camera"; const String kWindowEventNewPortForward = "new_port_forward"; const String kWindowEventNewTerminal = "new_terminal"; +const String kWindowEventRestoreTerminalSessions = "restore_terminal_sessions"; const String kWindowEventActiveSession = "active_session"; const String kWindowEventActiveDisplaySession = "active_display_session"; const String kWindowEventGetRemoteList = "get_remote_list"; diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart index ee252910738..60f20e8b0b5 100644 --- a/flutter/lib/desktop/pages/terminal_tab_page.dart +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -171,6 +171,8 @@ class _TerminalTabPageState extends State { forceRelay: args['forceRelay'], connToken: args['connToken'], )); + } else if (call.method == kWindowEventRestoreTerminalSessions) { + _restoreSessions(call.arguments); } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { @@ -188,6 +190,32 @@ class _TerminalTabPageState extends State { super.dispose(); } + Future _restoreSessions(String arguments) async { + Map? args; + try { + args = jsonDecode(arguments) as Map; + } catch (e) { + debugPrint("Error parsing JSON arguments in _restoreSessions: $e"); + return; + } + final persistentSessions = + args['persistent_sessions'] as List? ?? []; + final sortedSessions = persistentSessions.whereType().toList()..sort(); + for (final terminalId in sortedSessions) { + _addNewTerminalForCurrentPeer(terminalId: terminalId); + // A delay is required to ensure the UI has sufficient time to update + // before adding the next terminal. Without this delay, `_TerminalPageState::dispose()` + // may be called prematurely while the tab widget is still in the tab controller. + // This behavior is likely due to a race condition between the UI rendering lifecycle + // and the addition of new tabs. Attempts to use `_TerminalPageState::addPostFrameCallback()` + // to wait for the previous page to be ready were unsuccessful, as the observed call sequence is: + // `initState() 2 -> dispose() 2 -> postFrameCallback() 2`, followed by `initState() 3`. + // The `Future.delayed` approach mitigates this issue by introducing a buffer period, + // allowing the UI to stabilize before proceeding. + await Future.delayed(const Duration(milliseconds: 300)); + } + } + bool _handleKeyEvent(KeyEvent event) { if (event is KeyDownEvent) { // Use Cmd+T on macOS, Ctrl+Shift+T on other platforms @@ -276,17 +304,20 @@ class _TerminalTabPageState extends State { return false; } - void _addNewTerminal(String peerId) { + void _addNewTerminal(String peerId, {int? terminalId}) { // Find first tab for this peer to get connection parameters final firstTab = tabController.state.value.tabs.firstWhere( (tab) => tab.key.startsWith('$peerId\_'), ); if (firstTab.page is TerminalPage) { final page = firstTab.page as TerminalPage; - final terminalId = _nextTerminalId++; + final newTerminalId = terminalId ?? _nextTerminalId++; + if (terminalId != null && terminalId >= _nextTerminalId) { + _nextTerminalId = terminalId + 1; + } tabController.add(_createTerminalTab( peerId: peerId, - terminalId: terminalId, + terminalId: newTerminalId, password: page.password, isSharedPassword: page.isSharedPassword, forceRelay: page.forceRelay, @@ -295,12 +326,12 @@ class _TerminalTabPageState extends State { } } - void _addNewTerminalForCurrentPeer() { + void _addNewTerminalForCurrentPeer({int? terminalId}) { final currentTab = tabController.state.value.selectedTabInfo; final parts = currentTab.key.split('_'); if (parts.isNotEmpty) { final peerId = parts[0]; - _addNewTerminal(peerId); + _addNewTerminal(peerId, terminalId: terminalId); } } diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart index 3284c539bb5..ef47300976d 100644 --- a/flutter/lib/models/terminal_model.dart +++ b/flutter/lib/models/terminal_model.dart @@ -1,7 +1,10 @@ import 'dart:async'; import 'dart:convert'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/main.dart'; import 'package:xterm/xterm.dart'; import 'model.dart'; @@ -195,6 +198,17 @@ class TerminalModel with ChangeNotifier { debugPrint('[TerminalModel] Error processing buffered input: $e'); notifyListeners(); }); + + final persistentSessions = + evt['persistent_sessions'] as List? ?? []; + if (kWindowId != null && persistentSessions.isNotEmpty) { + DesktopMultiWindow.invokeMethod( + kWindowId!, + kWindowEventRestoreTerminalSessions, + jsonEncode({ + 'persistent_sessions': persistentSessions, + })); + } } else { terminal.write('Failed to open terminal: $message\r\n'); } diff --git a/libs/hbb_common b/libs/hbb_common index 25e761f4677..f91459c4ab8 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 25e761f46778b567061770bc64d66332a4503332 +Subproject commit f91459c4ab80fc3cfdef0882b2af51f984bc914c diff --git a/src/flutter.rs b/src/flutter.rs index e3c3c8c0daf..602f5701a81 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1119,6 +1119,9 @@ impl InvokeUiSession for FlutterHandler { ("pid", json!(opened.pid)), ("service_id", json!(&opened.service_id)), ]; + if !opened.persistent_sessions.is_empty() { + event_data.push(("persistent_sessions", json!(opened.persistent_sessions))); + } self.push_event_("terminal_response", &event_data, &[], &[]); } Some(Union::Data(data)) => { diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index e369de8f863..a1ff5f18ef6 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -131,6 +131,8 @@ fn get_or_create_service( // Ensure cleanup task is running ensure_cleanup_task(); + service.lock().unwrap().needs_session_sync = true; + Ok(service) } @@ -540,6 +542,7 @@ pub struct PersistentTerminalService { pub created_at: Instant, last_activity: Instant, pub is_persistent: bool, + needs_session_sync: bool, } impl PersistentTerminalService { @@ -550,6 +553,7 @@ impl PersistentTerminalService { created_at: Instant::now(), last_activity: Instant::now(), is_persistent, + needs_session_sync: false, } } @@ -696,6 +700,19 @@ impl TerminalServiceProxy { if self.is_persistent { opened.service_id = self.service_id.clone(); } + if service.needs_session_sync { + if service.sessions.len() > 1 { + // No need to include the current terminal in the list. + // Because the `persistent_sessions` is used to restore the other sessions. + opened.persistent_sessions = service + .sessions + .keys() + .filter(|&id| *id != open.terminal_id) + .cloned() + .collect(); + } + service.needs_session_sync = false; + } response.set_opened(opened); // Send buffered output @@ -856,6 +873,12 @@ impl TerminalServiceProxy { if self.is_persistent { opened.service_id = service.service_id.clone(); } + if service.needs_session_sync { + if !service.sessions.is_empty() { + opened.persistent_sessions = service.sessions.keys().cloned().collect(); + } + service.needs_session_sync = false; + } response.set_opened(opened); log::info!( From e91f4fc1048b8d8f47b31cebf0aef39d406182ed Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:25:53 +0800 Subject: [PATCH 416/506] fix: terminal, restore, cross users (#12335) Signed-off-by: fufesou --- src/client.rs | 10 +++++++++- src/client/io_loop.rs | 7 +++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index 48d753756e4..b7f3611a706 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2551,7 +2551,7 @@ impl LoginConfigHandler { }), ConnType::TERMINAL => { let mut terminal = Terminal::new(); - terminal.service_id = self.get_option("terminal-service-id"); + terminal.service_id = self.get_option(self.get_key_terminal_service_id()); lr.set_terminal(terminal); } _ => {} @@ -2602,6 +2602,14 @@ impl LoginConfigHandler { pub fn get_id(&self) -> &str { &self.id } + + pub fn get_key_terminal_service_id(&self) -> &'static str { + if self.is_terminal_admin { + "terminal-admin-service-id" + } else { + "terminal-service-id" + } + } } /// Media data. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index e2838cc2218..29b7601cad9 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1944,10 +1944,9 @@ impl Remote { use hbb_common::message_proto::terminal_response::Union; if let Some(Union::Opened(opened)) = &response.union { if opened.success && !opened.service_id.is_empty() { - self.handler.lc.write().unwrap().set_option( - "terminal-service-id".to_owned(), - opened.service_id.clone(), - ); + let mut lc = self.handler.lc.write().unwrap(); + let key = lc.get_key_terminal_service_id().to_owned(); + lc.set_option(key, opened.service_id.clone()); } } self.handler.handle_terminal_response(response); From 2e2b4ac2fe4b67d2e7511f5a77375f67c9979d25 Mon Sep 17 00:00:00 2001 From: John Fowler Date: Fri, 18 Jul 2025 12:14:47 +0200 Subject: [PATCH 417/506] Update hu.rs (#12323) Translate new strings. --- src/lang/hu.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 974f85831fe..78fee43e7d2 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -703,12 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Terminál engedélyezése"), ("New tab", "Új lap"), ("Keep terminal sessions on disconnect", "Terminál munkamenetek megtartása leválasztáskor"), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), + ("Terminal (Run as administrator)", "Terminál (rendszergazdaként futtatva)"), + ("terminal-admin-login-tip", "Kérjük, adja meg a felügyelt terminál rendszergazdai fiókjának jelszavát."), + ("Failed to get user token.", "Hiba a felhasználói token lekérdezésekor."), + ("Incorrect username or password.", "A felhasználónév vagy a jelszó helytelen."), + ("The user is not an administrator.", "A felhasználó nem rendszergazda."), + ("Failed to check if the user is an administrator.", "Hiba merült fel annak ellenőrzése során, hogy a felhasználó rendszergazda-e."), + ("Supported only in the installed version.", "Csak a telepített változatban támogatott."), ].iter().cloned().collect(); } From 0a62103ccda77383e5f13998d655b03ed4fb73cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Fri, 18 Jul 2025 19:15:01 +0900 Subject: [PATCH 418/506] Update ko.rs (#12316) --- src/lang/ko.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index e9b034eb794..a70ac83e35d 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -702,13 +702,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Terminal", "터미널"), ("Enable terminal", "터미널 사용함"), ("New tab", "새 탭"), - ("Keep terminal sessions on disconnect", "터미널 세션 연결 해제 상태 유지"), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), + ("Keep terminal sessions on disconnect", "연결이 끊어져도 터미널 세션 유지"), + ("Terminal (Run as administrator)", "터미널 (관리자 권한으로 실행)"), + ("terminal-admin-login-tip", "제어되는 측의 관리자 사용자 이름과 비밀번호를 입력하세요."), + ("Failed to get user token.", "사용자 토큰을 가져오는 데 실패했습니다."), + ("Incorrect username or password.", "사용자 이름이나 비밀번호가 올바르지 않습니다."), + ("The user is not an administrator.", "사용자가 관리자가 아닙니다."), + ("Failed to check if the user is an administrator.", "사용자가 관리자인지 확인하는 데 실패했습니다."), + ("Supported only in the installed version.", "설치된 버전에서만 지원됩니다."), ].iter().cloned().collect(); } From 061dc9962d5a71fe4aeea0a66ddf0a1e85ab7886 Mon Sep 17 00:00:00 2001 From: solokot Date: Fri, 18 Jul 2025 13:15:56 +0300 Subject: [PATCH 419/506] Update ru.rs (#12332) --- src/lang/ru.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index cf51cbaa73d..74a8b6751c1 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -703,12 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Включить терминал"), ("New tab", "Новая вкладка"), ("Keep terminal sessions on disconnect", "Сохранять сеансы терминала при отключении"), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), + ("Terminal (Run as administrator)", "Терминал (администратор)"), + ("terminal-admin-login-tip", "Введите имя пользователя и пароль администратора управляемой стороны."), + ("Failed to get user token.", "Невозможно получить токен пользователя."), + ("Incorrect username or password.", "Неправильное имя пользователя или пароль."), + ("The user is not an administrator.", "Пользователь не является администратором."), + ("Failed to check if the user is an administrator.", "Невозможно проверить, является ли пользователь администратором."), + ("Supported only in the installed version.", "Поддерживается только в установочной версии."), ].iter().cloned().collect(); } From 3177786219b6808036d42eab8038ad947c836c78 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:16:00 +0200 Subject: [PATCH 420/506] Update de.rs (#12324) --- 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 5d92a7a870b..d881e5a94bd 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -703,12 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Terminal zulassen"), ("New tab", "Neuer Tab"), ("Keep terminal sessions on disconnect", "Terminalsitzungen beim Trennen der Verbindung beibehalten"), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), + ("Terminal (Run as administrator)", "Terminal (als Administrator ausführen)"), + ("terminal-admin-login-tip", "Bitte geben Sie den Benutzernamen und das Passwort des Administrators der kontrollierten Seite ein."), + ("Failed to get user token.", "Benutzer-Token konnte nicht abgerufen werden."), + ("Incorrect username or password.", "Falscher Benutzername oder falsches Passwort."), + ("The user is not an administrator.", "Der Benutzer ist kein Administrator."), + ("Failed to check if the user is an administrator.", "Es konnte nicht geprüft werden, ob der Benutzer ein Administrator ist."), + ("Supported only in the installed version.", "Wird nur in der installierten Version unterstützt."), ].iter().cloned().collect(); } From 4723d072158e8c144dd0a6beab2c633e14ed7f4e Mon Sep 17 00:00:00 2001 From: XLion Date: Fri, 18 Jul 2025 18:16:28 +0800 Subject: [PATCH 421/506] Update tw.rs (#12327) * Update tw.rs * Update tw.rs --- src/lang/tw.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 55da035bb0d..ae398222740 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -699,16 +699,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable camera", "允許查看鏡頭"), ("No cameras", "沒有鏡頭"), ("view_camera_unsupported_tip", "您的遠端設備不支援查看鏡頭"), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), + ("Terminal", "終端機"), + ("Enable terminal", "啟用終端機"), + ("New tab", "新分頁"), + ("Keep terminal sessions on disconnect", "在斷線時保持終端機的工作階段"), + ("Terminal (Run as administrator)", "終端機(使用系統管理員執行)"), + ("terminal-admin-login-tip", "請輸入被控端系統管理員的使用者名稱與密碼"), + ("Failed to get user token.", "取得使用者權杖失敗"), + ("Incorrect username or password.", "使用者名稱或密碼不正確"), + ("The user is not an administrator.", "使用者並不是系統管理員"), + ("Failed to check if the user is an administrator.", "檢查使用者是否是系統管理員時失敗了"), + ("Supported only in the installed version.", "僅支援於已安裝的版本"), ].iter().cloned().collect(); } From a37f4d79db689b0fd420f89b029ccbec26caf5dd Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:16:59 +0200 Subject: [PATCH 422/506] Italian language update (#12321) --- 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 62907991463..f2dc6f0b91b 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -703,12 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Abilita terminale"), ("New tab", "Nuova scheda"), ("Keep terminal sessions on disconnect", "Quando disconetti mantieni attiva sessione terminale"), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), + ("Terminal (Run as administrator)", "Terminale (esegui come amministratore)"), + ("terminal-admin-login-tip", "Inserisci il nome utente e la password dell'amministratore del lato controllato."), + ("Failed to get user token.", "Impossibile ottenere il token utente."), + ("Incorrect username or password.", "Nome utente o password non corretti."), + ("The user is not an administrator.", "L'utente non è un amministratore."), + ("Failed to check if the user is an administrator.", "Impossibile verificare se l'utente è un amministratore."), + ("Supported only in the installed version.", "Supportato solo nella versione installata."), ].iter().cloned().collect(); } From 158127210411d47fd91cbd388dedc28e779beb46 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 18 Jul 2025 18:40:43 +0800 Subject: [PATCH 423/506] opt hint of elevation username (#12338) Signed-off-by: 21pages --- flutter/lib/common/widgets/dialog.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 | 1 + 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/ge.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/sc.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/ta.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/vi.rs | 2 +- 48 files changed, 48 insertions(+), 47 deletions(-) diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index fc2334d58b1..fe0b799ac49 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -1177,7 +1177,7 @@ void showRequestElevationDialog( DialogTextField( controller: userController, title: translate('Username'), - hintText: translate('eg: admin'), + hintText: translate('elevation_username_tip'), prefixIcon: DialogTextField.kUsernameIcon, errorText: errUser.isEmpty ? null : errUser.value, ), diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 66fbb953385..7ba1d35df2c 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "لا يوجد اقران مفضلين حتى الان؟\nحسنا لنبحث عن شخص للاتصال معه ومن ثم اضافته للمفضلة."), ("empty_lan_tip", "اه لا, يبدو انك لم تكتشف اي قرين بعد."), ("empty_address_book_tip", "يا عزيزي, يبدو انه لايوجد حاليا اي اقران في كتاب العناوين."), - ("eg: admin", "مثلا: admin"), ("Empty Username", "اسم مستخدم فارغ"), ("Empty Password", "كلمة مرور فارغة"), ("Me", "انا"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 22a9c3ced8a..d2254749257 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Яшчэ няма выбраных аддаленых вузлоў?\nДавайце знойдзем, каго можна дадаць у выбранае."), ("empty_lan_tip", "Не знойдзены аддаленыя вузлы."), ("empty_address_book_tip", "У адраснай кнізе няма аддаленых вузлоў."), - ("eg: admin", "напрыклад: admin"), ("Empty Username", "Пустае імя карыстальніка"), ("Empty Password", "Пусты пароль"), ("Me", "Я"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index f4d71f280d2..ffaf66aa984 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Все още нямате любими връзки?\nНека намерим някой, с когото да се свържете, и да го добавим към вашите любими!"), ("empty_lan_tip", "О, не, изглежда, че все още не сме открили връзки."), ("empty_address_book_tip", "Изглежда, че в момента няма изброени връзки във вашата адресна книга."), - ("eg: admin", "напр. admin"), ("Empty Username", "Празно потребителско име"), ("Empty Password", "Празна парола"), ("Me", "Аз"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 125b1c77db0..772a0baa78d 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", "Vós"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 2d4e0ee2cc5..be4321419ae 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "还没有收藏的被控端?找一个人连接并将其添加到收藏吧!"), ("empty_lan_tip", "情况不妙,似乎未发现任何被控端!"), ("empty_address_book_tip", "似乎目前地址簿内无被控端"), - ("eg: admin", "例如:admin"), ("Empty Username", "空用户名"), ("Empty Password", "空密码"), ("Me", "我"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "用户不是管理员。"), ("Failed to check if the user is an administrator.", "检查用户是否为管理员时出错。"), ("Supported only in the installed version.", "仅在以安装版本受支持。"), + ("elevation_username_tip", "输入用户名或域名\\用户名"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index dfe66bb5bfe..3f1c0b753fd 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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é."), - ("eg: admin", "např. admin"), ("Empty Username", "Prázdné uživatelské jméno"), ("Empty Password", "Prázdné heslo"), ("Me", "Já"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index c09f54260c6..f3e212eec3b 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index d881e5a94bd..683fd2dd7ae 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Noch keine favorisierte Gegenstelle?\nLassen Sie uns jemanden finden, mit dem wir uns verbinden können und fügen Sie ihn zu Ihren Favoriten hinzu!"), ("empty_lan_tip", "Oh nein, es sieht so aus, als hätten wir noch keine Gegenstelle entdeckt."), ("empty_address_book_tip", "Oh je, es scheint, dass in Ihrem Adressbuch derzeit keine Gegenstellen aufgeführt sind."), - ("eg: admin", "z. B.: admin"), ("Empty Username", "Leerer Benutzername"), ("Empty Password", "Leeres Passwort"), ("Me", "Ich"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "Der Benutzer ist kein Administrator."), ("Failed to check if the user is an administrator.", "Es konnte nicht geprüft werden, ob der Benutzer ein Administrator ist."), ("Supported only in the installed version.", "Wird nur in der installierten Version unterstützt."), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 0404661a54b..9f5f7be0c3d 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Δεν υπάρχουν ακόμη αγαπημένες συνδέσεις;\nΑφού πραγματοποιήσετε σύνδεση με κάποιο απομακρυσμένο σταθμό, μπορείτε να τον προσθέσετε στα αγαπημένα σας!"), ("empty_lan_tip", "Δεν έχουμε ανακαλυφθεί ακόμη απομακρυσμένοι σταθμοί."), ("empty_address_book_tip", "Φαίνεται ότι αυτή τη στιγμή δεν υπάρχουν αγαπημένες συνδέσεις στο βιβλίο διευθύνσεών σας."), - ("eg: admin", "π.χ. admin"), ("Empty Username", "Κενό όνομα χρήστη"), ("Empty Password", "Κενός κωδικός πρόσβασης"), ("Me", "Εγώ"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 14904f076be..dafa8f0702c 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -257,5 +257,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("update-failed-check-msi-tip", "Installation method check failed. Please click the \"Download\" button to download from the release page and upgrade manually."), ("websocket_tip", "When using WebSocket, only relay connections are supported."), ("terminal-admin-login-tip", "Please input the administrator username and password of the controlled side."), + ("elevation_username_tip", "Input username or domain\\username"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index f6ddf2fcfbe..912faa74483 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 9f93854be27..78cdb0a9bf1 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "¿Sin pares favoritos aún?\nEncontremos uno al que conectarte y ¡añádelo a tus favoritos!"), ("empty_lan_tip", "Oh no, parece que aún no has descubierto ningún par."), ("empty_address_book_tip", "Parece que actualmente no hay pares en tu directorio."), - ("eg: admin", "ej.: admin"), ("Empty Username", "Nombre de usuario vacío"), ("Empty Password", "Contraseña vacía"), ("Me", "Yo"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index b5ba0892861..d8ac432819b 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Ei ole veel ühtegi lemmikpartnerit?\nLeia keegi, kellega suhelda ja lisa ta oma lemmikute hulka!"), ("empty_lan_tip", "Oh ei, tundub, et me pole veel ühtegi partnerit avastanud."), ("empty_address_book_tip", "Oh ei, tundub et sinu aadressiraamatus ei ole hetkel ühtegi partnerit."), - ("eg: admin", "nt admin"), ("Empty Username", "Tühi kasutajanimi"), ("Empty Password", "Tühi parool"), ("Me", "Mina"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 1ad0466f986..d50291fde7c 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Parekide gogokorik gabe oraindik?\nBilatu norbait konektatzeko eta gehitu zure gogokoetara!"), ("empty_lan_tip", "Ai ez, badirudi ez duzula parekiderik aurkitu oraindik."), ("empty_address_book_tip", "Badirudi ez dagoela parekiderik zure helbide-liburuan."), - ("eg: admin", "adib. admin"), ("Empty Username", "Erabiltzaile-izena hutsik"), ("Empty Password", "Pasahitza hutsik"), ("Me", "Ni"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 18eeded8e53..073f406a68a 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "هنوز همتای مورد علاقه‌ای ندارید؟\nبیایید فردی را برای ارتباط پیدا کنیم و آن را به موارد دلخواه خود اضافه کنیم!"), ("empty_lan_tip", "اوه نه، به نظر می رسد که ما هنوز همتای خود را پیدا نکرده ایم"), ("empty_address_book_tip", "اوه ، به نظر می رسد که در حال حاضر هیچ همتایی در دفترچه آدرس شما وجود ندارد"), - ("eg: admin", "مثال : admin"), ("Empty Username", "نام کاربری خالی است"), ("Empty Password", "رمز عبور خالی است"), ("Me", "من"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index ea6dea5459e..3ca48a25853 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Vous n’avez pas encore d’appareils distants favoris ?\nTrouvez quelqu’un avec qui vous connecter et ajoutez-le à vos favoris !"), ("empty_lan_tip", "Oh non, il semble que nous n’avons pas encore découvert d’appareils sur le réseau local."), ("empty_address_book_tip", "Mince, il n’y a actuellement aucun appareil distant répertorié dans votre carnet d’adresses."), - ("eg: admin", "ex : admin"), ("Empty Username", "Nom d’utilisation non renseigné"), ("Empty Password", "Mot de passe non renseigné"), ("Me", "Moi"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 2f227bbf9cb..3e72cc96fdc 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "ჯერ არ გაქვთ რჩეული დისტანციური კვანძები?\nმოდით, ვნახოთ, ვის შეიძლება დავამატოთ რჩეულებში!"), ("empty_lan_tip", "დისტანციური კვანძები ვერ მოიძებნა."), ("empty_address_book_tip", "მისამართების წიგნში არ არის დისტანციური კვანძები."), - ("eg: admin", "მაგ: admin"), ("Empty Username", "ცარიელი მომხმარებლის სახელი"), ("Empty Password", "ცარიელი პაროლი"), ("Me", "მე"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 20f0daf4a36..3d0efd5b619 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "עדיין אין עמיתים מועדפים?\nבא נמצא מישהו להתחבר אליו ונוסיף אותו למועדפים!"), ("empty_lan_tip", "אוי לא, נראה שעדיין לא גילינו עמיתים."), ("empty_address_book_tip", "אבוי, נראה שכרגע אין עמיתים בספר הכתובות שלך."), - ("eg: admin", "לדוגמה: admin"), ("Empty Username", "שם משתמש ריק"), ("Empty Password", "סיסמה ריקה"), ("Me", "אני"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 9f1aeecabb6..f04e2c10ae6 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Još nemate nijednog omiljenog partnera?\nPronađite nekoga s kim se možete povezati i dodajte ga u svoje favorite!"), ("empty_lan_tip", "Ali ne, izgleda da još nismo otkrili niti jednu drugu stranu."), ("empty_address_book_tip", "Izgleda da trenutno nemate nijednog kolege navedenog u svom imeniku."), - ("eg: admin", "napr. admin"), ("Empty Username", "Prazno korisničko ime"), ("Empty Password", "Prazna lozinka"), ("Me", "Ja"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 78fee43e7d2..ef37dd986dc 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "A felhasználó nem rendszergazda."), ("Failed to check if the user is an administrator.", "Hiba merült fel annak ellenőrzése során, hogy a felhasználó rendszergazda-e."), ("Supported only in the installed version.", "Csak a telepített változatban támogatott."), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 75d69f777ae..9ecaaeb3b8f 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Belum ada rekan favorit?\nTemukan seseorang untuk terhubung dan tambahkan ke favorit!"), ("empty_lan_tip", "Sepertinya kami belum memiliki rekan"), ("empty_address_book_tip", "Tampaknya saat ini tidak ada rekan yang terdaftar dalam buku alamat Anda"), - ("eg: admin", "contoh: admin"), ("Empty Username", "Nama pengguna kosong"), ("Empty Password", "Kata sandi kosong"), ("Me", "Saya"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index f2dc6f0b91b..0482a722369 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Ancora nessuna connessione?\nTrova qualcuno con cui connetterti e aggiungilo ai preferiti!"), ("empty_lan_tip", "Sembra proprio che non sia stata rilevata nessuna connessione."), ("empty_address_book_tip", "Sembra che per ora nella rubrica non ci siano connessioni."), - ("eg: admin", "es: admin"), ("Empty Username", "Nome utente vuoto"), ("Empty Password", "Password vuota"), ("Me", "Io"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "L'utente non è un amministratore."), ("Failed to check if the user is an administrator.", "Impossibile verificare se l'utente è un amministratore."), ("Supported only in the installed version.", "Supportato solo nella versione installata."), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index d6693564f2d..9e0852afa95 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "お気に入りのリモートコンピュータがないようですね?あなたの接続先を登録しましょう!"), ("empty_lan_tip", "あらら、まだ近くのコンピューターは発見できていないようです。"), ("empty_address_book_tip", "驚くべきことに、あなたのアドレス帳には現在コンピューターが登録されていません。"), - ("eg: admin", "例: 管理者"), ("Empty Username", "空のユーザー名"), ("Empty Password", "空のパスワード"), ("Me", "あなた"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index a70ac83e35d..3e861d210c7 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "장치 즐겨찾기가 없습니다. 새 즐겨찾기를 추가해보세요"), ("empty_lan_tip", "제어되는 장치가 발견되지 않았습니다."), ("empty_address_book_tip", "현재 주소록에 제어되는 클라이언트가 없습니다"), - ("eg: admin", "예: 관리자"), ("Empty Username", "사용자 이름이 비어있습니다"), ("Empty Password", "비밀번호가 비어있습니다"), ("Me", "나"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "사용자가 관리자가 아닙니다."), ("Failed to check if the user is an administrator.", "사용자가 관리자인지 확인하는 데 실패했습니다."), ("Supported only in the installed version.", "설치된 버전에서만 지원됩니다."), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index b7edc45650f..6c9bb7a498f 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 4911c75dec1..1cecafb7263 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Dar neturite parankinių nuotolinių seansų."), ("empty_lan_tip", "Nuotolinių mazgų nerasta."), ("empty_address_book_tip", "Adresų knygelėje nėra nuotolinių kompiuterių."), - ("eg: admin", "pvz.: administratorius"), ("Empty Username", "Tuščias naudotojo vardas"), ("Empty Password", "Tuščias slaptažodis"), ("Me", "Aš"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 2685590576b..3b1e0a2de8f 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Vēl nav iecienītākās sesijas?\nAtradīsim kādu, ar ko sazināties, un pievienosim to jūsu izlasei!"), ("empty_lan_tip", "Ak nē! Šķiet, ka mēs vēl neesam atklājuši nevienu sesiju."), ("empty_address_book_tip", "Ak vai, izskatās, ka jūsu adrešu grāmatā šobrīd nav neviena sesija."), - ("eg: admin", "piemēram: admin"), ("Empty Username", "Tukšs lietotājvārds"), ("Empty Password", "Tukša parole"), ("Me", "Es"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index d10c671dd77..3b69f9a1f7a 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", "f.eks.: admin"), ("Empty Username", "Tøm brukernavn"), ("Empty Password", "Tøm passord"), ("Me", "Meg"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index ec2cb387190..025a8f22cb6 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Nog geen favoriete stations op afstand? Laat ons iemand vinden om mee te verbinden en voeg hem toe aan uw favorieten!"), ("empty_lan_tip", "Oh nee, het lijkt erop dat we nog geen extern station hebben ontdekt."), ("empty_address_book_tip", "Oh jee, het lijkt erop dat er momenteel geen externe stations in uw adresboek staan."), - ("eg: admin", "bijvoorbeeld: admin"), ("Empty Username", "Gebruikersnaam Leeg"), ("Empty Password", "Wachtwoord Leeg"), ("Me", "Ik"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 619a8910039..913db786467 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Brak ulubionych?\nZnajdźmy kogoś, z kim możesz się połączyć i dodaj Go do ulubionych!"), ("empty_lan_tip", "Ojej, wygląda na to, że nie odkryliśmy żadnych urządzeń z RustDesk w Twojej sieci."), ("empty_address_book_tip", "Ojej, wygląda na to, że nie ma żadnych wpisów w Twojej książce adresowej."), - ("eg: admin", "np. admin"), ("Empty Username", "Pusty użytkownik"), ("Empty Password", "Puste hasło"), ("Me", "Ja"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index f8fe45d34e3..1012f26954f 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5257dd7f427..9ed8328ce6f 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Ainda não há parceiros favoritos?\nVamos encontrar alguém para se conectar e adicioná-lo aos seus favoritos!"), ("empty_lan_tip", "Ah não, parece que ainda não descobrimos nenhum parceiro."), ("empty_address_book_tip", "Oh céus, parece que atualmente não há parceiros listados em seu catálogo de endereços."), - ("eg: admin", "ex. admin"), ("Empty Username", "Nome de Usuário vazio"), ("Empty Password", "Senha Vazia"), ("Me", "Eu"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index b1fd7340d26..ad44894c16f 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Încă nu ai niciun dispozitiv pereche favorit?\nHai să-ți găsim pe cineva cu care să te conectezi, iar apoi poți adăuga dispozitivul la Favorite!"), ("empty_lan_tip", "Of! S-ar părea că încă nu am descoperit niciun dispozitiv."), ("empty_address_book_tip", "Măi să fie! Se pare că deocamdată nu figurează niciun dispozitiv în agenda ta."), - ("eg: admin", "ex: admin"), ("Empty Username", "Nume utilizator nespecificat"), ("Empty Password", "Parolă nespecificată"), ("Me", "Eu"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 74a8b6751c1..f3e80fedf71 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Ещё нет избранных удалённых узлов?\nДавайте найдём, кого можно добавить в избранное!"), ("empty_lan_tip", "Не найдено удалённых узлов."), ("empty_address_book_tip", "В адресной книге нет удалённых узлов."), - ("eg: admin", "например: admin"), ("Empty Username", "Пустое имя пользователя"), ("Empty Password", "Пустой пароль"), ("Me", "Я"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "Пользователь не является администратором."), ("Failed to check if the user is an administrator.", "Невозможно проверить, является ли пользователь администратором."), ("Supported only in the installed version.", "Поддерживается только в установочной версии."), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 8067c66acd4..525f3fc4f51 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Galu peruna connessione?\nBusca calicunu cun chie ti collegare e annanghe·lu a sos preferidos!"), ("empty_lan_tip", "Paret a beru chi non siat istada atzapada peruna connessione."), ("empty_address_book_tip", "Paret chi pro como in sa rubrica non b'apat connessiones."), - ("eg: admin", "es: admin"), ("Empty Username", "Nùmene utente bòidu"), ("Empty Password", "Crae bòida"), ("Me", "Deo"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 6a093d92473..9e935554fbc 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Ešte nemáte obľúbeného partnera?\nNájdite niekoho, s kým sa môžete spojiť, a pridajte si ho do obľúbených!"), ("empty_lan_tip", "Ale nie, zdá sa, že sme zatiaľ neobjavili žiadnu protistranu."), ("empty_address_book_tip", "Ach bože, zdá sa, že vo vašom adresári momentálne nie sú uvedení žiadni kolegovia."), - ("eg: admin", "napr. admin"), ("Empty Username", "Prázdne používateľské meno"), ("Empty Password", "Prázdne heslo"), ("Me", "Ja"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 59236932212..c81150f20d0 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index f01b913f670..52ccf2d9722 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 20fb6f5ee59..d60b21ba7c7 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index fe8589b565f..356ef13ade9 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 266dcf94f73..29f3e79145f 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "காலி_விருப்பமான_குறிப்பு"), ("empty_lan_tip", "காலி_லேன்_குறிப்பு"), ("empty_address_book_tip", "காலி_முகவரி_புத்தக_குறிப்பு"), - ("eg: admin", "எ.கா: admin"), ("Empty Username", "காலி பயனர்பெயர்"), ("Empty Password", "காலி கடவுச்சொல்"), ("Me", "நான்"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 5302d208c04..5407634898c 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 919a6d54cda..d64931aec39 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "ยังไม่มีการเชื่อมต่อรายการโปรดเหรอ? มาเริ่มต้นหาใครซักคนเพื่อเชื่อมต่อด้วย และเพิ่มเข้าไปยังรายการโปรดของคุณกัน"), ("empty_lan_tip", "ไม่นะ ดูเหมือนว่าเราจะยังไม่พบใครตรงนี้"), ("empty_address_book_tip", "ดูเหมือนว่าคุณยังไม่มีใครถูกบันทึกในสมุดรายชื่อของคุณ"), - ("eg: admin", "เช่น ผู้ดูแลระบบ"), ("Empty Username", "ชื่อผู้ใช้งานว่างเปล่า"), ("Empty Password", "รหัสผ่านว่างเปล่า"), ("Me", "ฉัน"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 19b7a4c60f9..f2af97fe3aa 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Henüz favori cihazınız yok mu?\nBağlanacak ve favorilere eklemek için birini bulalım!"), ("empty_lan_tip", "Hayır, henüz hiçbir cihaz bulamadık gibi görünüyor."), ("empty_address_book_tip", "Üzgünüm, şu anda adres defterinizde kayıtlı cihaz yok gibi görünüyor."), - ("eg: admin", "örn: admin"), ("Empty Username", "Boş Kullanıcı Adı"), ("Empty Password", "Boş Parola"), ("Me", "Ben"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index ae398222740..e062d34db6a 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "空空如也"), ("empty_lan_tip", "喔不,看來我們目前找不到任何夥伴。"), ("empty_address_book_tip", "老天,看來您的通訊錄中沒有任何夥伴。"), - ("eg: admin", "例如:admin"), ("Empty Username", "空使用者帳號"), ("Empty Password", "空密碼"), ("Me", "我"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "使用者並不是系統管理員"), ("Failed to check if the user is an administrator.", "檢查使用者是否是系統管理員時失敗了"), ("Supported only in the installed version.", "僅支援於已安裝的版本"), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index b0037857819..8c4ab0bb1ae 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Досі немає улюблених вузлів?\nДавайте організуємо нове підключення та додамо його до улюблених!"), ("empty_lan_tip", "О ні, схоже ми ще не виявили жодного віддаленого пристрою."), ("empty_address_book_tip", "Ой лишенько, схоже у вашій адресній книзі немає жодного віддаленого пристрою."), - ("eg: admin", "напр., admin"), ("Empty Username", "Незаповнене імʼя"), ("Empty Password", "Незаповнений пароль"), ("Me", "Я"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 44707f64bca..d301e4e1737 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -461,7 +461,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Chưa có người dùng yêu thích nào cả?\nHãy tìm ai đó để kết nối cùng và thêm họ vào danh sách yêu thích!"), ("empty_lan_tip", "Ôi không, có vẻ như chúng ta chưa phát hiện ra bất cứ người dùng nào cả."), ("empty_address_book_tip", "Ôi bạn ơi, có vẻ như bạn chưa thêm ai vào quyển địa chỉ cả."), - ("eg: admin", "ví dụ: admin"), ("Empty Username", "Tên tài khoản trống"), ("Empty Password", "Mật khẩu trống"), ("Me", "Tôi"), @@ -710,5 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", ""), ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), ].iter().cloned().collect(); } From 555bb666683d24d6d11b5bccda6b59edb42135b5 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 19 Jul 2025 11:14:14 +0800 Subject: [PATCH 424/506] fix: terminal, handle newline (#12342) Signed-off-by: fufesou --- flutter/lib/models/terminal_model.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart index ef47300976d..8f059c48613 100644 --- a/flutter/lib/models/terminal_model.dart +++ b/flutter/lib/models/terminal_model.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; import 'package:xterm/xterm.dart'; @@ -24,7 +25,20 @@ class TerminalModel with ChangeNotifier { final _inputBuffer = []; + bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows; + Future _handleInput(String data) async { + // If we press the `Enter` button on Android, + // `data` can be '\r' or '\n' when using different keyboards. + // Android -> Windows. '\r' works, but '\n' does not. '\n' is just a newline. + // Android -> Linux. Both '\r' and '\n' work as expected (execute a command). + // So when we receive '\n', we may need to convert it to '\r' to ensure compatibility. + // Desktop -> Desktop works fine. + // Check if we are on mobile or web(mobile), and convert '\n' to '\r'. + final isMobileOrWebMobile = (isMobile || (isWeb && !isWebDesktop)); + if (isMobileOrWebMobile && isPeerWindows && data == '\n') { + data = '\r'; + } if (_terminalOpened) { // Send user input to remote terminal try { From 9d82ef1a225c7c35053d632eeac9bc66c7187915 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 19 Jul 2025 14:23:22 +0800 Subject: [PATCH 425/506] remove terminal.md --- terminal.md | 521 ---------------------------------------------------- 1 file changed, 521 deletions(-) delete mode 100644 terminal.md diff --git a/terminal.md b/terminal.md deleted file mode 100644 index 92a0973cb63..00000000000 --- a/terminal.md +++ /dev/null @@ -1,521 +0,0 @@ -# RustDesk Terminal Service Implementation - -## Overview - -The RustDesk terminal service provides remote terminal/shell access with support for multiple concurrent terminal sessions per connection. It features persistence support, allowing terminal sessions to survive connection drops and be resumed later. - -## Architecture - -### Client-Side (Flutter) - -#### Terminal Connection Management -- **TerminalConnectionManager** (`flutter/lib/desktop/pages/terminal_connection_manager.dart`) - - Manages one FFI instance per peer (shared across all terminal tabs) - - Tracks persistence settings per peer - - Handles connection reference counting - -#### Terminal Models -- **TerminalModel** (`flutter/lib/models/terminal_model.dart`) - - One instance per terminal tab - - Handles terminal I/O and display using xterm package - - Manages terminal state (opened, size, buffer) - -#### UI Components -- **TerminalTabPage** (`flutter/lib/desktop/pages/terminal_tab_page.dart`) - - Manages multiple terminal tabs - - Right-click menu for persistence toggle - - Keyboard shortcuts (Cmd/Ctrl+Shift+T for new terminal) - -### Server-Side (Rust) - -#### Terminal Service Structure -```rust -TerminalService { - conn_id: i32, - service_id: String, // "tmp_{uuid}" or "persist_{uuid}" - persist: bool, -} - -PersistentTerminalService { - service_id: String, - sessions: HashMap, // terminal_id -> session - next_terminal_id: i32, - created_at: Instant, - last_activity: Instant, -} - -TerminalSession { - terminal_id: i32, - pty_pair: PtyPair, - child: Box, - writer: Box, - reader: Box, - output_buffer: OutputBuffer, // For reconnection - rows: u16, - cols: u16, -} -``` - -## Message Protocol - -### Client → Server Messages - -1. **Open Terminal** -```protobuf -TerminalAction { - open: OpenTerminal { - terminal_id: i32, - rows: u32, - cols: u32, - } -} -``` - -2. **Send Input** -```protobuf -TerminalAction { - data: TerminalData { - terminal_id: i32, - data: bytes, - } -} -``` - -3. **Resize Terminal** -```protobuf -TerminalAction { - resize: ResizeTerminal { - terminal_id: i32, - rows: u32, - cols: u32, - } -} -``` - -4. **Close Terminal** -```protobuf -TerminalAction { - close: CloseTerminal { - terminal_id: i32, - force: bool, - } -} -``` - -### Server → Client Messages - -1. **Terminal Opened** -```protobuf -TerminalResponse { - opened: TerminalOpened { - terminal_id: i32, - success: bool, - message: string, - pid: u32, - } -} -``` - -2. **Terminal Output** -```protobuf -TerminalResponse { - data: TerminalData { - terminal_id: i32, - data: bytes, // Base64 encoded in Flutter - } -} -``` - -3. **Terminal Closed** -```protobuf -TerminalResponse { - closed: TerminalClosed { - terminal_id: i32, - exit_code: i32, - } -} -``` - -## Persistence Design - -### Service ID Convention -- **Temporary**: `"tmp_{uuid}"` - Cleaned up after idle timeout -- **Persistent**: `"persist_{uuid}"` - Survives disconnections - -### Persistence Flow -1. User right-clicks terminal tab → "Enable terminal persistence" -2. Client stores persistence preference in `TerminalConnectionManager` -3. New terminals created with appropriate service ID prefix -4. Service ID saved for future reconnection (TODO: implement storage) - -### Cleanup Rules -- **Temporary services (`tmp_`)**: - - Removed after 1 hour idle time - - Immediately removed when service loop exits - -- **Persistent services**: - - Removed after 2 hours idle time IF empty - - Survive connection drops - - Can be reconnected using saved service ID - -### Cleanup Implementation - -#### 1. **Automatic Background Cleanup** -```rust -// Runs every 5 minutes -fn ensure_cleanup_task() { - tokio::spawn(async { - let mut interval = tokio::time::interval(Duration::from_secs(300)); - loop { - interval.tick().await; - cleanup_inactive_services(); - } - }); -} -``` - -#### 2. **Cleanup Logic** -```rust -fn cleanup_inactive_services() { - let now = Instant::now(); - - for (service_id, service) in services.iter() { - // Temporary services: clean up after 1 hour idle - if service_id.starts_with("tmp_") && - now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT { - to_remove.push(service_id); - } - // Persistent services: clean up after 2 hours IF empty - else if !service_id.starts_with("tmp_") && - svc.sessions.is_empty() && - now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT * 2 { - to_remove.push(service_id); - } - } -} -``` - -#### 3. **Service Loop Exit Cleanup** -```rust -fn run(sp: EmptyExtraFieldService, _conn_id: i32, service_id: String) { - // Service loop - while sp.ok() { - // Read and send terminal outputs... - } - - // Clean up temporary services immediately on exit - if service_id.starts_with("tmp_") { - remove_service(&service_id); - } -} -``` - -#### 4. **Session Cleanup Within Service** -When a terminal is closed: -- PTY process is terminated -- Terminal session removed from service's HashMap -- Resources (file descriptors, buffers) are freed -- Service continues running for other terminals - -#### 5. **Connection Drop Behavior** -```rust -impl Drop for Connection { - fn drop(&mut self) { - if self.terminal { - // Unsubscribe from terminal service - server.subscribe(&service_name, self.inner.clone(), false); - } - } -} -``` -- Connection unsubscribes from service -- Service loop continues if other subscribers exist -- If no subscribers remain, `sp.ok()` returns false → service loop exits - -#### 6. **Activity Tracking** -`last_activity` is updated when: -- New terminal opened -- Input sent to terminal -- Terminal resized -- Output read from terminal -- Any terminal operation occurs - -#### 7. **Two-Phase Cleanup Process** -```rust -// Collect services to remove (while holding lock) -let mut to_remove = Vec::new(); -for (id, service) in services.iter() { - if should_remove(service) { - to_remove.push(id); - } -} - -// Remove services (after releasing lock) -drop(services); -for id in to_remove { - remove_service(&id); -} -``` -This prevents deadlock when removing services. - -## Key Features - -### Multiple Terminals per Connection -- Single FFI connection shared by all terminal tabs -- Each terminal has unique ID within the service -- Independent PTY sessions per terminal - -### Output Buffering -- Last 1MB of output buffered per terminal -- Allows showing recent history on reconnection -- Ring buffer with line-based storage - -### Cross-Platform Support -- **Unix/Linux/macOS**: Uses default shell from `$SHELL` or `/bin/bash` -- **Windows**: Uses `%COMSPEC%` or `cmd.exe` -- PTY implementation via `portable_pty` crate - -### Non-Blocking I/O -- PTY readers set to non-blocking mode (Unix) -- Output polled at ~33fps for responsive display -- Prevents blocking when no data available - -## Current Limitations - -1. **Service ID Storage**: Client doesn't persist service IDs yet -2. **Reconnection UI**: No UI to recover previous sessions -3. **Authentication**: No per-service authentication for reconnection -4. **Resource Limits**: No configurable limits on terminals per service - -## Future Enhancements - -1. **Proper Reconnection Flow**: - - Store service IDs in peer config - - UI to list and recover previous sessions - - Show buffered output on reconnection - -2. **Security**: - - Authentication token for service recovery - - Encryption of buffered output - - Access control per terminal - -3. **Advanced Features**: - - Terminal sharing between users - - Session recording/playback - - File transfer via terminal - - Custom shell/command configuration - -## Code Locations - -- **Server Implementation**: `src/server/terminal_service.rs` -- **Connection Handler**: `src/server/connection.rs` (handle_terminal_action) -- **Client Interface**: `src/ui_session_interface.rs` (terminal methods) -- **Flutter FFI**: `src/flutter_ffi.rs` (session_open_terminal, etc.) -- **Flutter Models**: `flutter/lib/models/terminal_model.dart` -- **Flutter UI**: `flutter/lib/desktop/pages/terminal_*.dart` - -## Usage - -1. **Start Terminal Session**: - - Click terminal icon or use Ctrl/Cmd+Shift+T - - Terminal opens with default shell - -2. **Enable Persistence**: - - Right-click any terminal tab - - Select "Enable terminal persistence" - - All terminals for that peer become persistent - -3. **Multiple Terminals**: - - Click "+" button or Ctrl/Cmd+Shift+T - - Each terminal is independent - -4. **Reconnection** (TODO): - - Connect to same peer - - Previous terminals automatically restored - - Recent output displayed - -## Implementation Issues & TODOs - -### Critical Missing Features - -1. **Service ID Storage & Recovery** - - Need to store service_id in peer config when persistence enabled - - Pass service_id in LoginRequest for reconnection - - Handle service_id in server login flow - - Return terminal list in LoginResponse - -2. **Protocol Extensions Needed** - ```protobuf - // In LoginRequest - message Terminal { - string service_id = 1; // For reconnection - bool persistent = 2; // Request persistence - } - - // In LoginResponse - message TerminalServiceInfo { - string service_id = 1; - repeated TerminalSessionInfo sessions = 2; - } - ``` - -3. **Terminal Recovery Flow** - - Add RecoverTerminal action to restore specific terminal - - Send buffered output on reconnection - - Handle terminal size on recovery - - UI to show available terminals - -### Current Design Issues - -1. **Service Pattern Mismatch** - - Terminal service forced into broadcast service pattern - - Should be direct connection resource, not shared service - - Complex routing through service registry unnecessary - -2. **Global State Management** - - TERMINAL_SERVICES static HashMap may cause issues - - No proper service discovery mechanism - - Cleanup task is global, not per-connection - -3. **Resource Limits Missing** - - No limit on terminals per service - - No limit on buffer size per terminal - - No limit on total services - - Could lead to resource exhaustion - -4. **Security Concerns** - - No authentication for service recovery - - Service IDs are predictable (just UUID) - - No encryption of buffered terminal output - - No access control between users - -### Performance Optimizations Needed - -1. **Output Reading** - - Currently polls at 33fps regardless of activity - - Should use event-driven I/O (epoll/kqueue) - - Batch small outputs to reduce messages - -2. **Buffer Management** - - Ring buffer could be more efficient - - Consider compression for stored output - - Implement smart truncation (keep last N complete lines) - -3. **Message Overhead** - - Each output chunk creates new protobuf message - - Could batch multiple terminal outputs - - Consider streaming protocol for continuous output - -### Platform-Specific Issues - -1. **Windows** - - ConPTY support needs testing - - Non-blocking I/O handled differently - - Shell detection could be improved - -2. **Mobile (Android/iOS)** - - Terminal feature disabled by conditional compilation - - Need to evaluate mobile terminal support - - Touch keyboard integration needed - -### Testing Requirements - -1. **Unit Tests Needed** - - Terminal service lifecycle - - Cleanup logic edge cases - - Buffer management - - Message serialization - -2. **Integration Tests** - - Multi-terminal scenarios - - Reconnection flows - - Cleanup timing - - Resource limits - -3. **Stress Tests** - - Many terminals per connection - - Large output volumes - - Rapid connect/disconnect - - Long-running sessions - -### Alternative Designs to Consider - -1. **Direct Terminal Management** - ```rust - // In Connection struct - terminals: HashMap, - - // No service pattern, direct management - async fn handle_terminal_action(&mut self, action) { - match action { - Open => self.open_terminal(), - Data => self.terminal_input(), - // etc - } - } - ``` - -2. **Actor-Based Design** - - Each terminal as an actor - - Message passing for I/O - - Better isolation and error handling - -3. **Session Manager Service** - - One global terminal manager - - Connections request terminals from manager - - Cleaner separation of concerns - -### Documentation Gaps - -1. **API Documentation** - - Document all public methods - - Add examples for common operations - - Document error conditions - -2. **Configuration** - - Document all timeouts and limits - - How to configure shell/terminal - - Platform-specific settings - -3. **Troubleshooting Guide** - - Common issues and solutions - - Debug logging interpretation - - Performance tuning - -### Future Feature Ideas - -1. **Advanced Terminal Features** - - Terminal sharing (multiple users, one terminal) - - Session recording and playback - - File transfer through terminal (zmodem) - - Custom color schemes - - Font configuration - -2. **Integration Features** - - SSH key forwarding - - Environment variable injection - - Working directory synchronization - - Shell integration (prompt markers, etc) - -3. **Management Features** - - Terminal session monitoring - - Usage statistics - - Audit logging - - Rate limiting - -### Refactoring Suggestions - -1. **Separate Concerns** - - Split terminal_service.rs into multiple files - - Separate PTY management from service logic - - Extract buffer management to own module - -2. **Improve Error Handling** - - Use proper error types, not strings - - Add error recovery mechanisms - - Better error reporting to client - -3. **Configuration Management** - - Make timeouts configurable - - Add feature flags for experimental features - - Environment-based configuration \ No newline at end of file From 55ddb9751ab3669a08a9538c38a7937dbdf4ea75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Sat, 19 Jul 2025 15:25:47 +0900 Subject: [PATCH 426/506] Create DEVCONTAINER-KR.md (#12331) --- docs/DEVCONTAINER-KR.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 docs/DEVCONTAINER-KR.md diff --git a/docs/DEVCONTAINER-KR.md b/docs/DEVCONTAINER-KR.md new file mode 100644 index 00000000000..78d6849f975 --- /dev/null +++ b/docs/DEVCONTAINER-KR.md @@ -0,0 +1,14 @@ + +Docker 컨테이너에서 devcontainer가 시작된 후, 디버그 모드의 Linux 바이너리가 생성됩니다. + +현재 devcontainer는 디버그 모드와 릴리스 모드 모두에서 Linux 및 Android 빌드를 제공합니다. + +아래는 특정 빌드를 생성하기 위해 프로젝트 루트에서 실행하는 명령에 대한 표입니다. + +명령|빌드 유형|모드 +-|-|-| +`.devcontainer/build.sh --debug linux`|Linux|디버그 +`.devcontainer/build.sh --release linux`|Linux|출시 +`.devcontainer/build.sh --debug android`|android-arm64|디버그 +`.devcontainer/build.sh --release android`|android-arm64|출시 + From 94e23a6cd07c9c1cca75ad26a9d84c1ec8fcf1c4 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 19 Jul 2025 14:26:11 +0800 Subject: [PATCH 427/506] remove devcontainer.md --- docs/DEVCONTAINER-DE.md | 14 -------------- docs/DEVCONTAINER-IT.md | 14 -------------- docs/DEVCONTAINER-JP.md | 14 -------------- docs/DEVCONTAINER-KR.md | 14 -------------- docs/DEVCONTAINER-NL.md | 15 --------------- docs/DEVCONTAINER-NO.md | 14 -------------- docs/DEVCONTAINER-PL.md | 14 -------------- docs/DEVCONTAINER-TR.md | 12 ------------ docs/DEVCONTAINER.md | 14 -------------- 9 files changed, 125 deletions(-) delete mode 100644 docs/DEVCONTAINER-DE.md delete mode 100644 docs/DEVCONTAINER-IT.md delete mode 100644 docs/DEVCONTAINER-JP.md delete mode 100644 docs/DEVCONTAINER-KR.md delete mode 100644 docs/DEVCONTAINER-NL.md delete mode 100644 docs/DEVCONTAINER-NO.md delete mode 100644 docs/DEVCONTAINER-PL.md delete mode 100644 docs/DEVCONTAINER-TR.md delete mode 100644 docs/DEVCONTAINER.md diff --git a/docs/DEVCONTAINER-DE.md b/docs/DEVCONTAINER-DE.md deleted file mode 100644 index 2a0d73f1797..00000000000 --- a/docs/DEVCONTAINER-DE.md +++ /dev/null @@ -1,14 +0,0 @@ - -Nach dem Start von Dev-Container im Docker-Container wird ein Linux-Binrprogramm im Debug-Modus erstellt. - -Derzeit bietet Dev-Container Linux- und Android-Builds sowohl im Debug- als auch im Release-Modus an. - -Nachfolgend finden Sie eine Tabelle mit Befehlen, die im Stammverzeichnis des Projekts ausgefhrt werden mssen, um bestimmte Builds zu erstellen. - -Kommando|Build-Typ|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/DEVCONTAINER-IT.md b/docs/DEVCONTAINER-IT.md deleted file mode 100644 index 713c6fc3768..00000000000 --- a/docs/DEVCONTAINER-IT.md +++ /dev/null @@ -1,14 +0,0 @@ - -Dopo l'avvio di devcontainer nel contenitore docker, viene creato un binario linux in modalità debug. - -Attualmente devcontainer consente creazione build Linux e Android sia in modalità debug che in modalità rilascio. - -Di seguito è riportata la tabella dei comandi da eseguire dalla root del progetto per la creazione di build specifiche. - -Comando|Tipo build|Modo --|-|-| -`.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/DEVCONTAINER-JP.md b/docs/DEVCONTAINER-JP.md deleted file mode 100644 index d8a599bef8c..00000000000 --- a/docs/DEVCONTAINER-JP.md +++ /dev/null @@ -1,14 +0,0 @@ - -docker コンテナで devcontainer を起動すると、デバッグモードの linux バイナリが作成されます。 - -現在 devcontainer では、Linux と android のビルドをデバッグモードとリリースモードの両方で提供しています。 - -以下は、特定のビルドを作成するためにプロジェクトのルートから実行するコマンドの表になります。 - -コマンド|ビルド タイプ|モード --|-|-| -`.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/DEVCONTAINER-KR.md b/docs/DEVCONTAINER-KR.md deleted file mode 100644 index 78d6849f975..00000000000 --- a/docs/DEVCONTAINER-KR.md +++ /dev/null @@ -1,14 +0,0 @@ - -Docker 컨테이너에서 devcontainer가 시작된 후, 디버그 모드의 Linux 바이너리가 생성됩니다. - -현재 devcontainer는 디버그 모드와 릴리스 모드 모두에서 Linux 및 Android 빌드를 제공합니다. - -아래는 특정 빌드를 생성하기 위해 프로젝트 루트에서 실행하는 명령에 대한 표입니다. - -명령|빌드 유형|모드 --|-|-| -`.devcontainer/build.sh --debug linux`|Linux|디버그 -`.devcontainer/build.sh --release linux`|Linux|출시 -`.devcontainer/build.sh --debug android`|android-arm64|디버그 -`.devcontainer/build.sh --release android`|android-arm64|출시 - diff --git a/docs/DEVCONTAINER-NL.md b/docs/DEVCONTAINER-NL.md deleted file mode 100644 index cd6ae456d89..00000000000 --- a/docs/DEVCONTAINER-NL.md +++ /dev/null @@ -1,15 +0,0 @@ - -Na de start van devcontainer in docker container wordt een linux binaire in foutmodus aangemaakt. - -Momenteel biedt devcontainer linux en android builds in zowel foutopsporing- als uitgave modus. - -Hieronder staat de tabel met commando's die vanuit de root van het project moeten worden -uitgevoerd om specifieke builds te maken. - -Commando|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|debug - diff --git a/docs/DEVCONTAINER-NO.md b/docs/DEVCONTAINER-NO.md deleted file mode 100644 index 1d944ed5d6c..00000000000 --- a/docs/DEVCONTAINER-NO.md +++ /dev/null @@ -1,14 +0,0 @@ - -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/DEVCONTAINER-PL.md b/docs/DEVCONTAINER-PL.md deleted file mode 100644 index 0aae2b975e3..00000000000 --- a/docs/DEVCONTAINER-PL.md +++ /dev/null @@ -1,14 +0,0 @@ - -Po uruchomieniu devcontainer w kontenerze docker, tworzony jest plik binarny linux w trybue debugowania. - -Obecnie devcontainer oferuje kompilowanie wersji dla linux i android w obu trybach - debugowania i wersji finalnej. - -Poniżej tabela poleceń do uruchomienia z głównego folderu do tworzenia wybranych kompilacji. - -Polecenie|Typ kompilacji|Tryb --|-|-| -`.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|debug - diff --git a/docs/DEVCONTAINER-TR.md b/docs/DEVCONTAINER-TR.md deleted file mode 100644 index 7fc14ce5ee9..00000000000 --- a/docs/DEVCONTAINER-TR.md +++ /dev/null @@ -1,12 +0,0 @@ -Docker konteynerinde devcontainer'ın başlatılmasından sonra, hata ayıklama modunda bir Linux ikili dosyası oluşturulur. - -Şu anda devcontainer, hata ayıklama ve sürüm modunda hem Linux hem de Android derlemeleri sunmaktadır. - -Aşağıda, belirli derlemeler oluşturmak için projenin kökünden çalıştırılması gereken komutlar yer almaktadır. - -Komut | Derleme Türü | Mod --|-|- -`.devcontainer/build.sh --debug linux` | Linux | hata ayıklama -`.devcontainer/build.sh --release linux` | Linux | sürüm -`.devcontainer/build.sh --debug android` | Android-arm64 | hata ayıklama -`.devcontainer/build.sh --release android` | Android-arm64 | sürüm diff --git a/docs/DEVCONTAINER.md b/docs/DEVCONTAINER.md deleted file mode 100644 index 3d04fd3994e..00000000000 --- a/docs/DEVCONTAINER.md +++ /dev/null @@ -1,14 +0,0 @@ - -After the start of devcontainer in docker container, a linux binary in debug mode is created. - -Currently devcontainer offers linux and android builds in both debug and release mode. - -Below is the table on commands to run from root of the project for creating specific builds. - -Command|Build Type|Mode --|-|-| -`.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 - From 9bcfe9d14895dc25c73c70e9c6fe0dcc82f14662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Sun, 20 Jul 2025 22:59:26 +0900 Subject: [PATCH 428/506] Update README-KR.md (#12329) Update --- docs/README-KR.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/README-KR.md b/docs/README-KR.md index b015b4a4dbf..d2123982225 100644 --- a/docs/README-KR.md +++ b/docs/README-KR.md @@ -1,19 +1,19 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    빌드Docker구조 • - 스크린샷
    - [Українська] | [č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]
    - 이 README, RustDesk UI and RustDesk 문서를 귀하의 모국어로 번역하는 데 도움이 필요합니다 + 스냇샷
    + [English] | [Українська] | [česky] | [中文] | [Magyar] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
    + 이 README, RustDesk UIRustDesk 문서를 귀하의 모국어로 번역하는 데 도움이 필요합니다

    -> [!주의] +> [!Caution] > **오용 면책 조항:**
    > RustDesk의 개발자는 이 소프트웨어의 비윤리적 또는 불법적인 사용을 묵인하거나 지원하지 않습니다. 무단 액세스, 제어 또는 개인정보 침해와 같은 오용은 엄격하게 당사의 지침에 위배됩니다. 작성자는 응용 프로그램의 오용에 대해 책임을 지지 않습니다. -채팅: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) +우리와 채팅: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) @@ -21,7 +21,7 @@ Rust로 작성된 또 다른 원격 데스크톱 소프트웨어입니다. 구 ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk는 모든 분들의 기여를 환영합니다. 시작하는 데 도움이 필요하면. [CONTRIBUTING.md](docs/CONTRIBUTING.md)를 참조하세요.. +RustDesk는 모든 분들의 기여를 환영합니다. 시작하는 데 도움이 필요하면 [CONTRIBUTING-KR.md](CONTRIBUTING-KR.md)를 참조하세요. [**자주 묻는 질문**](https://github.com/rustdesk/rustdesk/wiki/FAQ) @@ -38,9 +38,9 @@ RustDesk는 모든 분들의 기여를 환영합니다. 시작하는 데 도움 ## 종속성 -데스크톱 버전은 GUI로 Flutter 또는 Sciter (더 이상 지원되지 않음)를 사용하며, 이 튜토리얼은 시작하기 더 쉽고 친숙한 Sciter 전용입니다. Flutter 버전 빌드는 [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)을 확인하세요.. +데스크톱 버전은 GUI로 Flutter 또는 Sciter (더 이상 지원되지 않음)를 사용하며, 이 자습서는 시작하기 더 쉽고 친숙한 Sciter 전용입니다. Flutter 버전 빌드는 [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)을 확인하세요. -Sciter 동적 라이브러리를 직접 다운로드하세요.. +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) | From 391ef70007300ab447ecdb374d678fe3f46bee5c Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:15:02 +0800 Subject: [PATCH 429/506] fix: terminal, persistent (#12357) Signed-off-by: fufesou --- flutter/lib/desktop/pages/terminal_tab_page.dart | 2 +- src/server/terminal_service.rs | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart index 60f20e8b0b5..0a681f58779 100644 --- a/flutter/lib/desktop/pages/terminal_tab_page.dart +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -124,7 +124,7 @@ class _TerminalTabPageState extends State { }, setter: (bool v) async { final ffi = Get.find(tag: 'terminal_$peerId'); - bind.sessionToggleOption( + await bind.sessionToggleOption( sessionId: ffi.sessionId, value: kOptionTerminalPersistent, ); diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index a1ff5f18ef6..558edc2f8c6 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -696,10 +696,7 @@ impl TerminalServiceProxy { opened.success = true; opened.message = "Reconnected to existing terminal".to_string(); opened.pid = session.pid; - // Return service_id for persistent sessions - if self.is_persistent { - opened.service_id = self.service_id.clone(); - } + opened.service_id = self.service_id.clone(); if service.needs_session_sync { if service.sessions.len() > 1 { // No need to include the current terminal in the list. @@ -869,10 +866,7 @@ impl TerminalServiceProxy { opened.success = true; opened.message = "Terminal opened".to_string(); opened.pid = session.pid; - // Return service_id for persistent sessions - if self.is_persistent { - opened.service_id = service.service_id.clone(); - } + opened.service_id = service.service_id.clone(); if service.needs_session_sync { if !service.sessions.is_empty() { opened.persistent_sessions = service.sessions.keys().cloned().collect(); From b65ef36049f43b00ba645e8c4c26501fd63753f8 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 22 Jul 2025 09:59:20 +0800 Subject: [PATCH 430/506] fix: terminal, restore, multi-sessions, msgs (#12364) Signed-off-by: fufesou --- src/server/terminal_service.rs | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index 558edc2f8c6..8bcd4c24618 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -131,7 +131,7 @@ fn get_or_create_service( // Ensure cleanup task is running ensure_cleanup_task(); - service.lock().unwrap().needs_session_sync = true; + service.lock().unwrap().reset_status(); Ok(service) } @@ -447,6 +447,7 @@ pub struct TerminalSession { cols: u16, // Track if we've already sent the closed message closed_message_sent: bool, + is_opened: bool, } impl TerminalSession { @@ -467,6 +468,7 @@ impl TerminalSession { rows, cols, closed_message_sent: false, + is_opened: false, } } @@ -477,6 +479,7 @@ impl TerminalSession { // This helper function is to ensure that the threads are joined before the child process is dropped. // Though this is not strictly necessary on macOS. fn stop(&mut self) { + self.is_opened = false; self.exiting.store(true, Ordering::SeqCst); // Drop the input channel to signal writer thread to exit @@ -596,6 +599,14 @@ impl PersistentTerminalService { pub fn has_active_terminals(&self) -> bool { !self.sessions.is_empty() } + + fn reset_status(&mut self) { + self.needs_session_sync = true; + for session in self.sessions.values() { + let mut session = session.lock().unwrap(); + session.is_opened = false; + } + } } pub struct TerminalServiceProxy { @@ -690,7 +701,8 @@ impl TerminalServiceProxy { // Check if terminal already exists if let Some(session_arc) = service.sessions.get(&open.terminal_id) { // Reconnect to existing terminal - let session = session_arc.lock().unwrap(); + let mut session = session_arc.lock().unwrap(); + session.is_opened = true; let mut opened = TerminalOpened::new(); opened.terminal_id = open.terminal_id; opened.success = true; @@ -860,6 +872,7 @@ impl TerminalServiceProxy { session.output_rx = Some(output_rx); session.reader_thread = Some(reader_thread); session.writer_thread = Some(writer_thread); + session.is_opened = true; let mut opened = TerminalOpened::new(); opened.terminal_id = open.terminal_id; @@ -997,6 +1010,17 @@ impl TerminalServiceProxy { } } } + // It's Ok to put the closed message here. + // Because the `reader_thread` is joined in `stop()`, + // and `stop()` is called before the session is dropped. + if should_send_closed { + closed_terminals.push(terminal_id); + } + + if !session.is_opened { + // Skip the session if it is not opened. + continue; + } // Read from output channel let mut has_activity = false; @@ -1041,10 +1065,6 @@ impl TerminalServiceProxy { if has_activity { session.update_activity(); } - - if should_send_closed { - closed_terminals.push(terminal_id); - } } } From 9bca5ac000b696a7802f7f61af2ebc368fff5523 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:16:13 +0800 Subject: [PATCH 431/506] refact: terminal, save window pos on close (#12370) Signed-off-by: fufesou --- flutter/lib/utils/multi_window_manager.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index a7b06b5c79d..95044eb74d3 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -460,9 +460,13 @@ class RustDeskMultiWindowManager { if (windows.isEmpty) { return; } - for (final wId in windows) { - debugPrint("closing multi window, type: ${type.toString()} id: $wId"); - await saveWindowPosition(type, windowId: wId); + for (int i = 0; i < windows.length; i++) { + final wId = windows[i]; + final shouldSavePos = type != WindowType.Terminal || i == windows.length - 1; + if (shouldSavePos) { + debugPrint("closing multi window, type: ${type.toString()} id: $wId"); + await saveWindowPosition(type, windowId: wId); + } try { await WindowController.fromWindowId(wId).setPreventClose(false); await WindowController.fromWindowId(wId).close(); From 61194182ebf4d00bb11dbda45810ddfa02a7765b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 22 Jul 2025 19:26:50 +0800 Subject: [PATCH 432/506] fix: debug, terminal web (#12375) Signed-off-by: fufesou --- flutter/lib/common/widgets/peer_card.dart | 4 +- flutter/lib/common/widgets/toolbar.dart | 2 +- .../lib/desktop/pages/connection_page.dart | 2 +- flutter/lib/mobile/pages/settings_page.dart | 4 +- flutter/lib/models/model.dart | 2 +- flutter/lib/models/terminal_model.dart | 53 ++++++++++++++++++- flutter/lib/web/bridge.dart | 16 ++++-- src/server/terminal_service.rs | 5 +- 8 files changed, 73 insertions(+), 15 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 4b52e6c46d0..db9f7af008f 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -551,7 +551,7 @@ abstract class BasePeerCard extends StatelessWidget { MenuEntryBase _terminalAction(BuildContext context) { return _connectCommonAction( context, - translate('Terminal'), + '${translate('Terminal')} (beta)', isTerminal: true, ); } @@ -560,7 +560,7 @@ abstract class BasePeerCard extends StatelessWidget { MenuEntryBase _terminalRunAsAdminAction(BuildContext context) { return _connectCommonAction( context, - translate('Terminal (Run as administrator)'), + '${translate('Terminal (Run as administrator)')} (beta)', isTerminalRunAsAdmin: true, ); } diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index ee05e52a322..cf5ed5c97bb 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -183,7 +183,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); v.add( TTextMenu( - child: Text(translate('Terminal')), + child: Text('${translate('Terminal')} (beta)'), onPressed: () => connectWithToken(isTerminal: true)), ); v.add( diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 41553b8db2d..6f672a75942 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -563,7 +563,7 @@ class _ConnectionPageState extends State () => onConnect(isViewCamera: true) ), ( - 'Terminal', + '${translate('Terminal')} (beta)', () => onConnect(isTerminal: true) ), ] diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 505b0ff04b4..5c9d28383e2 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -378,7 +378,7 @@ class _SettingsState extends State with WidgetsBindingObserver { }, ), SettingsTile.switchTile( - title: Text('${translate('Adaptive bitrate')} (beta)'), + title: Text(translate('Adaptive bitrate')), initialValue: _enableAbr, onToggle: isOptionFixed(kOptionEnableAbr) ? null @@ -540,7 +540,7 @@ class _SettingsState extends State with WidgetsBindingObserver { enhancementsTiles.add(SettingsTile.switchTile( initialValue: _enableStartOnBoot, title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("${translate('Start on boot')} (beta)"), + Text(translate('Start on boot')), Text( '* ${translate('Start the screen sharing service on boot, requires special permissions')}', style: Theme.of(context).textTheme.bodySmall), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 017f2c9d11b..c6118efa195 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -3214,7 +3214,7 @@ class FFI { } void routeTerminalResponse(Map evt) { - final int terminalId = evt['terminal_id'] ?? 0; + final int terminalId = TerminalModel.getTerminalIdFromEvt(evt); // Route to specific terminal model if it exists final model = _terminalModels[terminalId]; diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart index 8f059c48613..ae64e818320 100644 --- a/flutter/lib/models/terminal_model.dart +++ b/flutter/lib/models/terminal_model.dart @@ -165,9 +165,58 @@ class TerminalModel with ChangeNotifier { } } + static int getTerminalIdFromEvt(Map evt) { + if (evt.containsKey('terminal_id')) { + final v = evt['terminal_id']; + if (v is int) { + // Desktop and mobile send terminal_id as an int + return v; + } else if (v is String) { + // Web sends terminal_id as a string + final parsed = int.tryParse(v); + if (parsed != null) { + return parsed; + } else { + debugPrint( + '[TerminalModel] Failed to parse terminal_id as integer: $v. Expected a numeric string.'); + return 0; + } + } else { + // Unexpected type, log and handle gracefully + debugPrint( + '[TerminalModel] Unexpected terminal_id type: ${v.runtimeType}, value: $v. Expected int or String.'); + return 0; + } + } else { + debugPrint('[TerminalModel] Event does not contain terminal_id'); + return 0; + } + } + + static bool getSuccessFromEvt(Map evt) { + if (evt.containsKey('success')) { + final v = evt['success']; + if (v is bool) { + // Desktop and mobile + return v; + } else if (v is String) { + // Web + return v.toLowerCase() == 'true'; + } else { + // Unexpected type, log and handle gracefully + debugPrint( + '[TerminalModel] Unexpected success type: ${v.runtimeType}, value: $v. Expected bool or String.'); + return false; + } + } else { + debugPrint('[TerminalModel] Event does not contain success'); + return false; + } + } + void handleTerminalResponse(Map evt) { final String? type = evt['type']; - final int evtTerminalId = evt['terminal_id'] ?? 0; + final int evtTerminalId = getTerminalIdFromEvt(evt); // Only handle events for this terminal if (evtTerminalId != terminalId) { @@ -193,7 +242,7 @@ class TerminalModel with ChangeNotifier { } void _handleTerminalOpened(Map evt) { - final bool success = evt['success'] ?? false; + final bool success = getSuccessFromEvt(evt); final String message = evt['message'] ?? ''; final String? serviceId = evt['service_id']; diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index f1839c6305d..388fba5da22 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -908,8 +908,18 @@ class RustdeskImpl { return js.context.callMethod('getByName', ['option:local', key]); } + // Do not return the real environment variables. + // Use the global variable as the environment variable in web. String mainGetEnv({required String key, dynamic hint}) { - throw UnimplementedError("mainGetEnv"); + return js.context.callMethod('getByName', ['envvar', key]); + } + + // Use the global variable as the environment variable in web. + void mainSetEnv({required String key, String? value, dynamic hint}) { + js.context.callMethod('setByName', [ + 'envvar', + jsonEncode({'name': key, 'value': value}) + ]); } Future mainSetLocalOption( @@ -1960,9 +1970,7 @@ class RustdeskImpl { } Future sessionCloseTerminal( - {required UuidValue sessionId, - required int terminalId, - dynamic hint}) { + {required UuidValue sessionId, required int terminalId, dynamic hint}) { return Future(() => js.context.callMethod('setByName', [ 'close_terminal', jsonEncode({ diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index 8bcd4c24618..3a3bcdb87e2 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -131,7 +131,7 @@ fn get_or_create_service( // Ensure cleanup task is running ensure_cleanup_task(); - service.lock().unwrap().reset_status(); + service.lock().unwrap().reset_status(is_persistent); Ok(service) } @@ -600,7 +600,8 @@ impl PersistentTerminalService { !self.sessions.is_empty() } - fn reset_status(&mut self) { + fn reset_status(&mut self, is_persistent: bool) { + self.is_persistent = is_persistent; self.needs_session_sync = true; for session in self.sessions.values() { let mut session = session.lock().unwrap(); From 348c477f75d8fcc2c953d83c8ccae3487ed948f3 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 22 Jul 2025 23:42:05 +0800 Subject: [PATCH 433/506] fix: terminal, web, fonts (#12376) Signed-off-by: fufesou --- flutter/lib/mobile/pages/terminal_page.dart | 19 +++++++++++++++++++ flutter/pubspec.lock | 8 ++++++++ flutter/pubspec.yaml | 1 + 3 files changed, 28 insertions(+) diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart index d7d17994c3a..e1e06c26c7f 100644 --- a/flutter/lib/mobile/pages/terminal_page.dart +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/terminal_model.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:xterm/xterm.dart'; import '../../desktop/pages/terminal_connection_manager.dart'; @@ -31,6 +32,12 @@ class _TerminalPageState extends State late FFI _ffi; late TerminalModel _terminalModel; + // For web only. + // 'monospace' does not work on web, use Google Fonts, `??` is only for null safety. + final String _robotoMonoFontFamily = isWeb + ? (GoogleFonts.robotoMono().fontFamily ?? 'monospace') + : 'monospace'; + @override void initState() { super.initState(); @@ -81,6 +88,7 @@ class _TerminalPageState extends State _terminalModel.terminal, controller: _terminalModel.terminalController, autofocus: true, + textStyle: _getTerminalStyle(), backgroundOpacity: 0.7, padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0), onSecondaryTapDown: (details, offset) async { @@ -101,6 +109,17 @@ class _TerminalPageState extends State ); } + // https://github.com/TerminalStudio/xterm.dart/issues/42#issuecomment-877495472 + // https://github.com/TerminalStudio/xterm.dart/issues/198#issuecomment-2526548458 + TerminalStyle _getTerminalStyle() { + return isWeb + ? TerminalStyle( + fontFamily: _robotoMonoFontFamily, + fontSize: 14, + ) + : const TerminalStyle(); + } + @override bool get wantKeepAlive => true; } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index aba6c7879f6..c6f8aa1c20a 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -689,6 +689,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" graphs: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 03de1a4eb01..72ea2701527 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -108,6 +108,7 @@ dependencies: extended_text: 14.0.0 xterm: 4.0.0 sqflite: 2.2.0 + google_fonts: ^6.2.1 dev_dependencies: icons_launcher: ^2.0.4 From 47886c4068eb4918e97ac541ada3877964b7bbd4 Mon Sep 17 00:00:00 2001 From: flusheDData <116861809+flusheDData@users.noreply.github.com> Date: Wed, 23 Jul 2025 05:12:16 +0200 Subject: [PATCH 434/506] Update es.rs (#12339) New terms added --- src/lang/es.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 78cdb0a9bf1..08be1c89b88 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -699,16 +699,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No cameras", "No hay cámaras"), ("view_camera_unsupported_tip", "El dispositivo remoto no soporta la visualización de la cámara."), ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), - ("elevation_username_tip", ""), + ("Enable terminal", "Habilitar terminal"), + ("New tab", "Nueva pestaña"), + ("Keep terminal sessions on disconnect", "Mantener sesiones de terminal al desconectar"), + ("Terminal (Run as administrator)", "Terminal (Ejecutar como administrador)"), + ("terminal-admin-login-tip", "Por favor, introduzca el usuario y la contrasseña del administrador en el lado controlado."), + ("Failed to get user token.", "No se ha podido obtener el token de usuario"), + ("Incorrect username or password.", "Nombre y contraseña incorrectos"), + ("The user is not an administrator.", "El usuario no es un administrador."), + ("Failed to check if the user is an administrator.", "No se ha podido comprobar si el usuario es un administrador."), + ("Supported only in the installed version.", "Soportado solo en la versión instalada."), + ("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario""), ].iter().cloned().collect(); } From c01bbeea7832b8b10e8afc9c6980991c65d1d741 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Wed, 23 Jul 2025 05:12:56 +0200 Subject: [PATCH 435/506] Italian language update (#12347) --- 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 0482a722369..fcebe35b45f 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "L'utente non è un amministratore."), ("Failed to check if the user is an administrator.", "Impossibile verificare se l'utente è un amministratore."), ("Supported only in the installed version.", "Supportato solo nella versione installata."), - ("elevation_username_tip", ""), + ("elevation_username_tip", "Inserisci Nome utente o dominio sorgente\\nome Utente"), ].iter().cloned().collect(); } From 596e7b33db872d95cda729e3b2484029e2eaa20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Wed, 23 Jul 2025 12:13:20 +0900 Subject: [PATCH 436/506] Update ko.rs (#12348) --- 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 3e861d210c7..91e6fda7096 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "사용자가 관리자가 아닙니다."), ("Failed to check if the user is an administrator.", "사용자가 관리자인지 확인하는 데 실패했습니다."), ("Supported only in the installed version.", "설치된 버전에서만 지원됩니다."), - ("elevation_username_tip", ""), + ("elevation_username_tip", "사용자 이름 또는 도메인\\사용자 이름 입력"), ].iter().cloned().collect(); } From 3fb3d51567c2f0d007a83b73fcc2bb55ac2d5c5d Mon Sep 17 00:00:00 2001 From: solokot Date: Wed, 23 Jul 2025 06:13:36 +0300 Subject: [PATCH 437/506] Update ru.rs (#12374) --- 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 f3e80fedf71..e40c93a7e37 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "Пользователь не является администратором."), ("Failed to check if the user is an administrator.", "Невозможно проверить, является ли пользователь администратором."), ("Supported only in the installed version.", "Поддерживается только в установочной версии."), - ("elevation_username_tip", ""), + ("elevation_username_tip", "Введите пользователя или домен\\пользователя"), ].iter().cloned().collect(); } From 80c4a83a39b45ef9458c6e6e319536b8ae1dd758 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:53:04 +0800 Subject: [PATCH 438/506] fix: build (#12385) Signed-off-by: fufesou --- 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 08be1c89b88..1365efa5875 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "El usuario no es un administrador."), ("Failed to check if the user is an administrator.", "No se ha podido comprobar si el usuario es un administrador."), ("Supported only in the installed version.", "Soportado solo en la versión instalada."), - ("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario""), + ("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario"), ].iter().cloned().collect(); } From 247f0b7eb130d35cc6d6a214a55c6a8f8afb5dd2 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:43:55 +0800 Subject: [PATCH 439/506] fix: terminal, check service_id (#12384) Signed-off-by: fufesou --- src/server/connection.rs | 25 ++++++++++++++++++++++++- src/server/terminal_service.rs | 16 ++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 7da62950826..e02b249180a 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1989,6 +1989,25 @@ impl Connection { sleep(1.).await; return false; } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(is_user) = + terminal_service::is_service_specified_user(&self.terminal_service_id) + { + if let Some(user_token) = &self.terminal_user_token { + let has_service_token = + user_token.to_terminal_service_token().is_some(); + if is_user != has_service_token { + // This occurs when the service id (in the configuration) is manually changed by the user, causing a mismatch in validation. + log::error!("Terminal service user mismatch detected. The service ID may have been manually changed in the configuration, causing validation to fail."); + // No need to translate the following message, because it is in an abnormal case. + self.send_login_error("Terminal service user mismatch detected.") + .await; + sleep(1.).await; + return false; + } + } + } } Some(login_request::Union::PortForward(mut pf)) => { if !Connection::permission("enable-tunnel") { @@ -2944,7 +2963,11 @@ impl Connection { } #[cfg(any(target_os = "linux", target_os = "macos"))] - fn fill_terminal_user_token(&mut self, _username: &str, _password: &str) -> Option<&'static str> { + fn fill_terminal_user_token( + &mut self, + _username: &str, + _password: &str, + ) -> Option<&'static str> { self.terminal_user_token = Some(TerminalUserToken::SelfUser); None } diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index 3a3bcdb87e2..945ae27bd09 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -98,10 +98,15 @@ fn get_default_shell() -> String { } } +pub fn is_service_specified_user(service_id: &str) -> Option { + get_service(service_id).map(|s| s.lock().unwrap().is_specified_user) +} + /// Get or create a persistent terminal service fn get_or_create_service( service_id: String, is_persistent: bool, + is_specified_user: bool, ) -> Result>> { let mut services = TERMINAL_SERVICES.lock().unwrap(); @@ -124,6 +129,7 @@ fn get_or_create_service( Arc::new(Mutex::new(PersistentTerminalService::new( service_id.clone(), is_persistent, + is_specified_user, ))) }) .clone(); @@ -306,7 +312,11 @@ pub fn new( user_token: Option, ) -> GenericService { // Create the service with initial persistence setting - allow_err!(get_or_create_service(service_id.clone(), is_persistent)); + allow_err!(get_or_create_service( + service_id.clone(), + is_persistent, + user_token.is_some() + )); let svc = TerminalService { sp: GenericService::new(service_id.clone(), false), user_token, @@ -546,10 +556,11 @@ pub struct PersistentTerminalService { last_activity: Instant, pub is_persistent: bool, needs_session_sync: bool, + is_specified_user: bool, } impl PersistentTerminalService { - pub fn new(service_id: String, is_persistent: bool) -> Self { + pub fn new(service_id: String, is_persistent: bool, is_specified_user: bool) -> Self { Self { service_id, sessions: HashMap::new(), @@ -557,6 +568,7 @@ impl PersistentTerminalService { last_activity: Instant::now(), is_persistent, needs_session_sync: false, + is_specified_user, } } From 50fc6d691ffc5684150a38e7373caa9125efb87e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 23 Jul 2025 15:51:44 +0800 Subject: [PATCH 440/506] 1.4.1 --- .github/workflows/flutter-build.yml | 2 +- .github/workflows/playground.yml | 2 +- .github/workflows/winget.yml | 4 ++-- 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 +- 13 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index c028844f678..56f8d93d83b 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -38,7 +38,7 @@ env: # https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174 # 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`. VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" - VERSION: "1.4.0" + VERSION: "1.4.1" 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 48e5c4df0dd..53e7f642ff4 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -17,7 +17,7 @@ env: TAG_NAME: "nightly" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" - VERSION: "1.4.0" + VERSION: "1.4.1" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 6fb0e9f4e81..2b1bff105a4 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -10,6 +10,6 @@ jobs: - uses: vedantmgoyal9/winget-releaser@main with: identifier: RustDesk.RustDesk - version: "1.4.0" - release-tag: "1.4.0" + version: "1.4.1" + release-tag: "1.4.1" token: ${{ secrets.WINGET_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index a5eee545ed3..2792b60d9fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6109,7 +6109,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.4.0" +version = "1.4.1" dependencies = [ "android-wakelock", "android_logger", @@ -6215,7 +6215,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.4.0" +version = "1.4.1" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index 7eb796d8600..d8403e14347 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.4.0" +version = "1.4.1" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 36297f0e18e..f228aac4239 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.4.0 + version: 1.4.1 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 59bcca92bda..602787e580e 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.4.0 + version: 1.4.1 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 72ea2701527..d8e1aff2cad 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.4.0+58 +version: 1.4.1+59 environment: sdk: '^3.1.0' diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index 2855b3cb6a7..8802ab306da 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.4.0" +version = "1.4.1" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index a5601bf31c8..269ded858a6 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.4.0 +pkgver=1.4.1 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 1f566c6ecd4..dd6b42c16b1 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.4.0 +Version: 1.4.1 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 7323c92f263..b461507da0b 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.4.0 +Version: 1.4.1 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index 0a64cbb3ca4..a51646631a5 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.4.0 +Version: 1.4.1 Release: 0 Summary: RPM package License: GPL-3.0 From f2473974b80ccacafc987c80ec920c552efdbe96 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 23 Jul 2025 17:10:26 +0800 Subject: [PATCH 441/506] fix ci (#12387) Signed-off-by: 21pages --- .github/workflows/flutter-build.yml | 14 ++++++-------- appimage/AppImageBuilder-aarch64.yml | 1 + appimage/AppImageBuilder-x86_64.yml | 1 + 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 56f8d93d83b..b1d75152077 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -23,7 +23,7 @@ env: 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" + SCITER_NASM_DEBVERSION: "2.15.05-1" LLVM_VERSION: "15.0.6" FLUTTER_VERSION: "3.24.5" ANDROID_FLUTTER_VERSION: "3.24.5" @@ -1978,11 +1978,8 @@ jobs: # https://github.com/AppImage/AppImageKit/wiki/FUSE sudo apt-get install -y libarchive-tools libfuse2 # set-up appimage-builder - pushd /tmp - wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage - chmod +x appimage-builder-x86_64.AppImage - sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder - popd + # https://github.com/AppImage/AppImageKit/issues/1395 + sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git # run appimage-builder pushd appimage sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml @@ -2009,14 +2006,15 @@ jobs: job: - { target: x86_64-unknown-linux-gnu, - distro: ubuntu18.04, + # https://github.com/ostreedev/ostree/commit/4bac96a8c817beda37448f9b8c662162bb619981 + distro: ubuntu22.04, on: ubuntu-22.04, arch: x86_64, suffix: "", } - { target: x86_64-unknown-linux-gnu, - distro: ubuntu18.04, + distro: ubuntu22.04, on: ubuntu-22.04, arch: x86_64, suffix: "-sciter", diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index f228aac4239..c7b8cfee1d1 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -99,3 +99,4 @@ AppDir: AppImage: arch: aarch64 update-information: guess + comp: gzip diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 602787e580e..4025f1669ef 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -102,3 +102,4 @@ AppDir: AppImage: arch: x86_64 update-information: guess + comp: gzip From b4e13706bd1b084e8c28877a71c817dacff77056 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:44:05 +0800 Subject: [PATCH 442/506] refact: active terminal on conn the same remote (#12392) Signed-off-by: fufesou --- flutter/lib/desktop/pages/terminal_tab_page.dart | 12 ++++++++++++ flutter/lib/utils/multi_window_manager.dart | 12 +++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart index 0a681f58779..754b309aec1 100644 --- a/flutter/lib/desktop/pages/terminal_tab_page.dart +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -177,6 +177,18 @@ class _TerminalTabPageState extends State { tabController.clear(); } else if (call.method == kWindowActionRebuild) { reloadCurrentWindow(); + } else if (call.method == kWindowEventActiveSession) { + if (tabController.state.value.tabs.isEmpty) { + return false; + } + final currentTab = tabController.state.value.selectedTabInfo; + assert(call.arguments is String, + "Expected String arguments for kWindowEventActiveSession, got ${call.arguments.runtimeType}"); + if (currentTab.key.startsWith(call.arguments)) { + windowOnTop(windowId()); + return true; + } + return false; } }); Future.delayed(Duration.zero, () { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 95044eb74d3..3bbb292f449 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -354,6 +354,16 @@ class RustDeskMultiWindowManager { bool? forceRelay, String? connToken, }) async { + // Iterate through terminal windows in reverse order to prioritize + // the most recently added or used windows, as they are more likely + // to have an active session. + for (final windowId in _terminalWindows.reversed) { + if (await DesktopMultiWindow.invokeMethod( + windowId, kWindowEventActiveSession, remoteId)) { + return MultiWindowCallResult(windowId, null); + } + } + // Terminal windows should always create new windows, not reuse // This avoids the MissingPluginException when trying to invoke // new_terminal on an inactive window @@ -366,7 +376,7 @@ class RustDeskMultiWindowManager { "connToken": connToken, }; final msg = jsonEncode(params); - + // Always create a new window for terminal final windowId = await newSessionWindow( WindowType.Terminal, remoteId, msg, _terminalWindows, false); From 1b40d146ee60ec8faaf617b2a90754a46d23344e Mon Sep 17 00:00:00 2001 From: TheBitBrine Date: Thu, 24 Jul 2025 04:51:25 +0400 Subject: [PATCH 443/506] Fix retry button blocked by overly broad "exist" filter (#12397) The retry logic was blocking retry buttons for errors containing "exist", which incorrectly filtered out "An existing connection was forcibly closed" network errors. Changed to "not exist" to only block "ID does not exist" type errors while allowing legitimate network disconnection errors to show retry buttons. Fixes issue where users couldn't retry after network disconnections. --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index b7f3611a706..073bf53ffd6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3693,7 +3693,7 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b && title == "Connection Error" && ((text.contains("10054") || text.contains("104")) && retry_for_relay || (!text.to_lowercase().contains("offline") - && !text.to_lowercase().contains("exist") + && !text.to_lowercase().contains("not exist") && !text.to_lowercase().contains("handshake") && !text.to_lowercase().contains("failed") && !text.to_lowercase().contains("resolve") From ab48f10f2574a265e1cd7d6cec4038633bb9f8ed Mon Sep 17 00:00:00 2001 From: John Fowler Date: Thu, 24 Jul 2025 11:43:06 +0200 Subject: [PATCH 444/506] Update hu.rs (#12403) Translate new string(s). --- src/lang/hu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index ef37dd986dc..fcb1958721b 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "A felhasználó nem rendszergazda."), ("Failed to check if the user is an administrator.", "Hiba merült fel annak ellenőrzése során, hogy a felhasználó rendszergazda-e."), ("Supported only in the installed version.", "Csak a telepített változatban támogatott."), - ("elevation_username_tip", ""), + ("elevation_username_tip", "Felhasználónév vagy tartománynév megadása\\felhasználónév"), ].iter().cloned().collect(); } From 2afd538cf1eafd013efc428ec21e10a8e71f709f Mon Sep 17 00:00:00 2001 From: XLion Date: Fri, 25 Jul 2025 13:13:31 +0800 Subject: [PATCH 445/506] Update tw.rs (#12412) --- 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 e062d34db6a..9e2703c83ec 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "使用者並不是系統管理員"), ("Failed to check if the user is an administrator.", "檢查使用者是否是系統管理員時失敗了"), ("Supported only in the installed version.", "僅支援於已安裝的版本"), - ("elevation_username_tip", ""), + ("elevation_username_tip", "輸入使用者名稱或網域\\使用者名稱"), ].iter().cloned().collect(); } From 9409912344bc5106c6a84e058fdc769aba657c9b Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 25 Jul 2025 13:22:52 +0800 Subject: [PATCH 446/506] update kcp-sys (#12419) 1. Update kcp-sys to send KCP in frames to avoid potential crashes. 2. Fix the issue when the controling side is closed, the kcp connection close is not immediately recognized by the controlled end. * Unless the controling side receives the close reason, force the sending of the close reason to the controlled end when using KCP, and delay for 30ms to ensure the message is sent successfully. * Move the CloseReason receiving forward, as this message needs to be received when unauthorized, especially for kcp. Signed-off-by: 21pages --- Cargo.lock | 3 ++- src/client/io_loop.rs | 30 ++++++++++++++++++++++++------ src/server/connection.rs | 19 ++++++++++--------- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2792b60d9fb..c00b0ade108 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3651,7 +3651,7 @@ dependencies = [ [[package]] name = "kcp-sys" version = "0.1.0" -source = "git+https://github.com/rustdesk-org/kcp-sys#1e5e30ab8b8c2f7787ab0f88822de36476531562" +source = "git+https://github.com/rustdesk-org/kcp-sys#32a6c09fc6223f54aea83981a6aa8995931d29be" dependencies = [ "anyhow", "auto_impl", @@ -3660,6 +3660,7 @@ dependencies = [ "bytes", "cc", "dashmap 6.1.0", + "log", "parking_lot", "rand 0.8.5", "thiserror 2.0.11", diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 29b7601cad9..4de0e7e3293 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -77,6 +77,7 @@ pub struct Remote { video_threads: HashMap, chroma: Arc>>, last_record_state: bool, + sent_close_reason: bool, } #[derive(Default)] @@ -125,6 +126,7 @@ impl Remote { video_threads: Default::default(), chroma: Default::default(), last_record_state: false, + sent_close_reason: false, } } @@ -172,7 +174,7 @@ impl Remote { ) .await { - Ok(((mut peer, direct, pk, _kcp), (feedback, rendezvous_server))) => { + Ok(((mut peer, direct, pk, kcp), (feedback, rendezvous_server))) => { self.handler .connection_round_state .lock() @@ -320,6 +322,13 @@ impl Remote { if let Some(s) = self.stop_voice_call_sender.take() { s.send(()).ok(); } + if kcp.is_some() { + // Send the close reason if it hasn't been sent yet, as KCP cannot detect the socket close event. + self.send_close_reason(&mut peer, "kcp").await; + // KCP does not send messages immediately, so wait to ensure the last message is sent. + // 1ms works in my test, but 30ms is more reliable. + tokio::time::sleep(Duration::from_millis(30)).await; + } } Err(err) => { self.handler.on_establish_connection_error(err.to_string()); @@ -511,14 +520,22 @@ impl Remote { } } + async fn send_close_reason(&mut self, peer: &mut Stream, reason: &str) { + if self.sent_close_reason { + return; + } + let mut misc = Misc::new(); + misc.set_close_reason(reason.to_owned()); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + self.sent_close_reason = true; + } + async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { match data { Data::Close => { - let mut misc = Misc::new(); - misc.set_close_reason("".to_owned()); - let mut msg = Message::new(); - msg.set_misc(misc); - allow_err!(peer.send(&msg).await); + self.send_close_reason(peer, "").await; return false; } Data::Login((os_username, os_password, password, remember)) => { @@ -1712,6 +1729,7 @@ impl Remote { } } Some(misc::Union::CloseReason(c)) => { + self.sent_close_reason = true; // The controlled end will close, no need to send close reason self.handler.msgbox("error", "Connection Error", &c, ""); return false; } diff --git a/src/server/connection.rs b/src/server/connection.rs index e02b249180a..01d84437db0 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1937,6 +1937,16 @@ impl Connection { } async fn on_message(&mut self, msg: Message) -> bool { + if let Some(message::Union::Misc(misc)) = &msg.union { + // Move the CloseReason forward, as this message needs to be received when unauthorized, especially for kcp. + if let Some(misc::Union::CloseReason(s)) = &misc.union { + log::info!("receive close reason: {}", s); + self.on_close("Peer close", true).await; + raii::AuthedConnID::check_remove_session(self.inner.id(), self.session_key()); + return false; + } + } + // After handling CloseReason messages, proceed to process other message types if let Some(message::Union::LoginRequest(lr)) = msg.union { self.handle_login_request_without_validation(&lr).await; if self.authorized { @@ -2790,15 +2800,6 @@ impl Connection { Some(Instant::now().into()), ); } - Some(misc::Union::CloseReason(_)) => { - self.on_close("Peer close", true).await; - raii::AuthedConnID::check_remove_session( - self.inner.id(), - self.session_key(), - ); - return false; - } - Some(misc::Union::RestartRemoteDevice(_)) => { #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.restart { From 2282c8e30897a93ae8bb97933001767599b71564 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 26 Jul 2025 18:41:57 +0800 Subject: [PATCH 447/506] opt assert for debug (#12420) Signed-off-by: 21pages --- flutter/lib/common.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index f54b88e88da..fda3f84e3ea 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1583,7 +1583,9 @@ String bool2option(String option, bool b) { option == kOptionForceAlwaysRelay) { res = b ? 'Y' : defaultOptionNo; } else { - assert(false); + if (option != kOptionEnableUdpPunch && option != kOptionEnableIpv6Punch) { + assert(false); + } res = b ? 'Y' : 'N'; } return res; From 52bfc02eeaf4156e43d121c61e5fef6cdaddb688 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 26 Jul 2025 12:42:19 +0200 Subject: [PATCH 448/506] Update de.rs (#12424) --- 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 683fd2dd7ae..f2ffe79bae1 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -709,6 +709,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The user is not an administrator.", "Der Benutzer ist kein Administrator."), ("Failed to check if the user is an administrator.", "Es konnte nicht geprüft werden, ob der Benutzer ein Administrator ist."), ("Supported only in the installed version.", "Wird nur in der installierten Version unterstützt."), - ("elevation_username_tip", ""), + ("elevation_username_tip", "Geben Sie Benutzername oder Domäne\\Benutzername ein"), ].iter().cloned().collect(); } From 6e62c10fa06b21a5eb7dd461a2907eb1b4bc6cfc Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 27 Jul 2025 19:47:23 +0800 Subject: [PATCH 449/506] Fix/printer printable area (#12433) * fix: printer, printable area Signed-off-by: fufesou * refact: windows, sc config RustDesk --start= delayed-auto Signed-off-by: fufesou --------- Signed-off-by: fufesou --- .github/workflows/flutter-build.yml | 20 ++++++++++---------- res/msi/CustomActions/CustomActions.cpp | 10 ++++++++++ res/msi/CustomActions/ServiceUtils.cpp | 9 +++++++++ src/platform/windows.rs | 2 ++ 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index b1d75152077..3a7a5e82642 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -177,24 +177,24 @@ jobs: # Download printer driver files and extract them to ./rustdesk try { - Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4.zip -OutFile rustdesk_printer_driver_v4.zip + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4-1.4.zip -OutFile rustdesk_printer_driver_v4-1.4.zip Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/printer_driver_adapter.zip -OutFile printer_driver_adapter.zip Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/sha256sums -OutFile sha256sums # Check and move the files - $checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4\.zip$').Matches.Groups[1].Value - $downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4.zip -Algorithm SHA256 - $checksum_dll = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value - $downloadsum_dll = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256 - if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_dll -eq $downloadsum_dll.Hash) { - Write-Output "rustdesk_printer_driver_v4, checksums match, extract the file." - Expand-Archive rustdesk_printer_driver_v4.zip -DestinationPath . + $checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4-1.4\.zip$').Matches.Groups[1].Value + $downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4-1.4.zip -Algorithm SHA256 + $checksum_adapter = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value + $downloadsum_adapter = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256 + if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_adapter -eq $downloadsum_adapter.Hash) { + Write-Output "rustdesk_printer_driver_v4-1.4, checksums match, extract the file." + Expand-Archive rustdesk_printer_driver_v4-1.4.zip -DestinationPath . mkdir ./rustdesk/drivers - mv -Force .\rustdesk_printer_driver_v4 ./rustdesk/drivers/RustDeskPrinterDriver + mv -Force .\rustdesk_printer_driver_v4-1.4 ./rustdesk/drivers/RustDeskPrinterDriver Expand-Archive printer_driver_adapter.zip -DestinationPath . mv -Force .\printer_driver_adapter.dll ./rustdesk } elseif ($checksum_driver -ne $downloadsum_driver.Hash) { - Write-Output "rustdesk_printer_driver_v4, checksums do not match, ignore the file." + Write-Output "rustdesk_printer_driver_v4-1.4, checksums do not match, ignore the file." } else { Write-Output "printer_driver_adapter.dll, checksums do not match, ignore the file." } diff --git a/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp index fafbab6b5f6..1b825398d4d 100644 --- a/res/msi/CustomActions/CustomActions.cpp +++ b/res/msi/CustomActions/CustomActions.cpp @@ -765,6 +765,16 @@ void TryCreateStartServiceByShell(LPWSTR svcName, LPWSTR svcBinary, LPWSTR szSvc WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is created with shell.", svcName); } + hr = StringCchPrintfW(szCmd, cchCmd, L"/c sc config %ls start= delayed-auto", svcName); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "Failed to format delayed auto-start command for service: %ls, HRESULT: 0x%08X", svcName, hr); + } else { + hi = ShellExecuteW(NULL, L"open", L"cmd.exe", szCmd, NULL, SW_HIDE); + if ((int)hi <= 32) { + WcaLog(LOGMSG_STANDARD, "Failed to configure delayed auto-start for service with shell: %d, last error: 0x%08X.", (int)hi, GetLastError()); + } + } + // Query and log if the service is running. for (int k = 0; k < 10; ++k) { if (!QueryServiceStatusExW(svcName, &svcStatus)) { diff --git a/res/msi/CustomActions/ServiceUtils.cpp b/res/msi/CustomActions/ServiceUtils.cpp index 38d0d1d48d7..0d534d6f02d 100644 --- a/res/msi/CustomActions/ServiceUtils.cpp +++ b/res/msi/CustomActions/ServiceUtils.cpp @@ -49,6 +49,15 @@ bool MyCreateServiceW(LPCWSTR serviceName, LPCWSTR displayName, LPCWSTR binaryPa WcaLog(LOGMSG_STANDARD, "Service installed successfully\n"); } + SERVICE_DELAYED_AUTO_START_INFO delayedStart = { TRUE }; + if (!ChangeServiceConfig2W( + schService, + SERVICE_CONFIG_DELAYED_AUTO_START_INFO, + &delayedStart + )) { + WcaLog(LOGMSG_STANDARD, "Failed to configure delayed auto-start for service: %ls, Error: %d\n", serviceName, GetLastError()); + } + CloseServiceHandle(schService); CloseServiceHandle(schSCManager); return true; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index a00e9906bfc..5237b95f8b2 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -2885,6 +2885,7 @@ fn get_import_config(exe: &str) -> String { sc stop {app_name} sc delete {app_name} sc create {app_name} binpath= \"\\\"{exe}\\\" --import-config \\\"{config_path}\\\"\" start= auto DisplayName= \"{app_name} Service\" +sc config {app_name} start= delayed-auto sc start {app_name} sc stop {app_name} sc delete {app_name} @@ -2906,6 +2907,7 @@ if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{ap } else { format!(" sc create {app_name} binpath= \"\\\"{exe}\\\" --service\" start= auto DisplayName= \"{app_name} Service\" +sc config {app_name} start= delayed-auto sc start {app_name} ", app_name = crate::get_app_name()) From e9692b94cae072facb9a50cc99436b174525922b Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:38:19 +0800 Subject: [PATCH 450/506] Revert "Fix/printer printable area (#12433)" (#12441) This reverts commit 6e62c10fa06b21a5eb7dd461a2907eb1b4bc6cfc. --- .github/workflows/flutter-build.yml | 20 ++++++++++---------- res/msi/CustomActions/CustomActions.cpp | 10 ---------- res/msi/CustomActions/ServiceUtils.cpp | 9 --------- src/platform/windows.rs | 2 -- 4 files changed, 10 insertions(+), 31 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 3a7a5e82642..b1d75152077 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -177,24 +177,24 @@ jobs: # Download printer driver files and extract them to ./rustdesk try { - Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4-1.4.zip -OutFile rustdesk_printer_driver_v4-1.4.zip + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4.zip -OutFile rustdesk_printer_driver_v4.zip Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/printer_driver_adapter.zip -OutFile printer_driver_adapter.zip Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/sha256sums -OutFile sha256sums # Check and move the files - $checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4-1.4\.zip$').Matches.Groups[1].Value - $downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4-1.4.zip -Algorithm SHA256 - $checksum_adapter = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value - $downloadsum_adapter = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256 - if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_adapter -eq $downloadsum_adapter.Hash) { - Write-Output "rustdesk_printer_driver_v4-1.4, checksums match, extract the file." - Expand-Archive rustdesk_printer_driver_v4-1.4.zip -DestinationPath . + $checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4\.zip$').Matches.Groups[1].Value + $downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4.zip -Algorithm SHA256 + $checksum_dll = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value + $downloadsum_dll = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256 + if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_dll -eq $downloadsum_dll.Hash) { + Write-Output "rustdesk_printer_driver_v4, checksums match, extract the file." + Expand-Archive rustdesk_printer_driver_v4.zip -DestinationPath . mkdir ./rustdesk/drivers - mv -Force .\rustdesk_printer_driver_v4-1.4 ./rustdesk/drivers/RustDeskPrinterDriver + mv -Force .\rustdesk_printer_driver_v4 ./rustdesk/drivers/RustDeskPrinterDriver Expand-Archive printer_driver_adapter.zip -DestinationPath . mv -Force .\printer_driver_adapter.dll ./rustdesk } elseif ($checksum_driver -ne $downloadsum_driver.Hash) { - Write-Output "rustdesk_printer_driver_v4-1.4, checksums do not match, ignore the file." + Write-Output "rustdesk_printer_driver_v4, checksums do not match, ignore the file." } else { Write-Output "printer_driver_adapter.dll, checksums do not match, ignore the file." } diff --git a/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp index 1b825398d4d..fafbab6b5f6 100644 --- a/res/msi/CustomActions/CustomActions.cpp +++ b/res/msi/CustomActions/CustomActions.cpp @@ -765,16 +765,6 @@ void TryCreateStartServiceByShell(LPWSTR svcName, LPWSTR svcBinary, LPWSTR szSvc WcaLog(LOGMSG_STANDARD, "Service \"%ls\" is created with shell.", svcName); } - hr = StringCchPrintfW(szCmd, cchCmd, L"/c sc config %ls start= delayed-auto", svcName); - if (FAILED(hr)) { - WcaLog(LOGMSG_STANDARD, "Failed to format delayed auto-start command for service: %ls, HRESULT: 0x%08X", svcName, hr); - } else { - hi = ShellExecuteW(NULL, L"open", L"cmd.exe", szCmd, NULL, SW_HIDE); - if ((int)hi <= 32) { - WcaLog(LOGMSG_STANDARD, "Failed to configure delayed auto-start for service with shell: %d, last error: 0x%08X.", (int)hi, GetLastError()); - } - } - // Query and log if the service is running. for (int k = 0; k < 10; ++k) { if (!QueryServiceStatusExW(svcName, &svcStatus)) { diff --git a/res/msi/CustomActions/ServiceUtils.cpp b/res/msi/CustomActions/ServiceUtils.cpp index 0d534d6f02d..38d0d1d48d7 100644 --- a/res/msi/CustomActions/ServiceUtils.cpp +++ b/res/msi/CustomActions/ServiceUtils.cpp @@ -49,15 +49,6 @@ bool MyCreateServiceW(LPCWSTR serviceName, LPCWSTR displayName, LPCWSTR binaryPa WcaLog(LOGMSG_STANDARD, "Service installed successfully\n"); } - SERVICE_DELAYED_AUTO_START_INFO delayedStart = { TRUE }; - if (!ChangeServiceConfig2W( - schService, - SERVICE_CONFIG_DELAYED_AUTO_START_INFO, - &delayedStart - )) { - WcaLog(LOGMSG_STANDARD, "Failed to configure delayed auto-start for service: %ls, Error: %d\n", serviceName, GetLastError()); - } - CloseServiceHandle(schService); CloseServiceHandle(schSCManager); return true; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 5237b95f8b2..a00e9906bfc 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -2885,7 +2885,6 @@ fn get_import_config(exe: &str) -> String { sc stop {app_name} sc delete {app_name} sc create {app_name} binpath= \"\\\"{exe}\\\" --import-config \\\"{config_path}\\\"\" start= auto DisplayName= \"{app_name} Service\" -sc config {app_name} start= delayed-auto sc start {app_name} sc stop {app_name} sc delete {app_name} @@ -2907,7 +2906,6 @@ if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{ap } else { format!(" sc create {app_name} binpath= \"\\\"{exe}\\\" --service\" start= auto DisplayName= \"{app_name} Service\" -sc config {app_name} start= delayed-auto sc start {app_name} ", app_name = crate::get_app_name()) From 0646a5b3137f5d9c4478f787d896ba3745355a82 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 28 Jul 2025 11:16:04 +0800 Subject: [PATCH 451/506] try to fix reboot not working because retry too slow --- src/rendezvous_mediator.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 9dd1695f107..54a3362d10a 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -93,6 +93,7 @@ impl RendezvousMediator { } scrap::codec::test_av1(); loop { + let mut timeout = CONNECT_TIMEOUT; let conn_start_time = Instant::now(); *SOLVING_PK_MISMATCH.lock().await = "".to_owned(); if !config::option2bool("stop-service", &Config::get_option("stop-service")) @@ -106,7 +107,15 @@ impl RendezvousMediator { let server = server.clone(); futs.push(tokio::spawn(async move { if let Err(err) = Self::start(server, host).await { - log::error!("rendezvous mediator error: {err}"); + let err = format!("rendezvous mediator error: {err}"); + // When user reboot, there might be below error, waiting too long + // (CONNECT_TIMEOUT 18s) will make user think there is bug + if err.contains("10054") || err.contains("11001") { + // No such host is known. (os error 11001) + // An existing connection was forcibly closed by the remote host. (os error 10054): also happens for UDP + timeout = 3000; + } + log::error!("{err}"); } // SHOULD_EXIT here is to ensure once one exits, the others also exit. SHOULD_EXIT.store(true, Ordering::SeqCst); @@ -119,8 +128,8 @@ impl RendezvousMediator { Config::reset_online(); if !MANUAL_RESTARTED.load(Ordering::SeqCst) { let elapsed = conn_start_time.elapsed().as_millis() as u64; - if elapsed < CONNECT_TIMEOUT { - sleep(((CONNECT_TIMEOUT - elapsed) / 1000) as _).await; + if elapsed < timeout{ + sleep(((timeout - elapsed) / 1000) as _).await; } } else { // https://github.com/rustdesk/rustdesk/issues/12233 @@ -204,7 +213,7 @@ impl RendezvousMediator { log::debug!("Non-protobuf message bytes received: {:?}", bytes); } }, - Some(Err(e)) => bail!("Failed to receive next {}", e), // maybe socks5 tcp disconnected + Some(Err(e)) => bail!("Failed to receive next: {}", e), // maybe socks5 tcp disconnected None => { bail!("Socket receive none. Maybe socks5 server is down."); }, From d0651e32c5311d4546881658563e824d559c4808 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:42:30 +0800 Subject: [PATCH 452/506] fix: printer, printable area (#12442) Signed-off-by: fufesou --- .github/workflows/flutter-build.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index b1d75152077..3a7a5e82642 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -177,24 +177,24 @@ jobs: # Download printer driver files and extract them to ./rustdesk try { - Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4.zip -OutFile rustdesk_printer_driver_v4.zip + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4-1.4.zip -OutFile rustdesk_printer_driver_v4-1.4.zip Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/printer_driver_adapter.zip -OutFile printer_driver_adapter.zip Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/sha256sums -OutFile sha256sums # Check and move the files - $checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4\.zip$').Matches.Groups[1].Value - $downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4.zip -Algorithm SHA256 - $checksum_dll = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value - $downloadsum_dll = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256 - if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_dll -eq $downloadsum_dll.Hash) { - Write-Output "rustdesk_printer_driver_v4, checksums match, extract the file." - Expand-Archive rustdesk_printer_driver_v4.zip -DestinationPath . + $checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4-1.4\.zip$').Matches.Groups[1].Value + $downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4-1.4.zip -Algorithm SHA256 + $checksum_adapter = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value + $downloadsum_adapter = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256 + if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_adapter -eq $downloadsum_adapter.Hash) { + Write-Output "rustdesk_printer_driver_v4-1.4, checksums match, extract the file." + Expand-Archive rustdesk_printer_driver_v4-1.4.zip -DestinationPath . mkdir ./rustdesk/drivers - mv -Force .\rustdesk_printer_driver_v4 ./rustdesk/drivers/RustDeskPrinterDriver + mv -Force .\rustdesk_printer_driver_v4-1.4 ./rustdesk/drivers/RustDeskPrinterDriver Expand-Archive printer_driver_adapter.zip -DestinationPath . mv -Force .\printer_driver_adapter.dll ./rustdesk } elseif ($checksum_driver -ne $downloadsum_driver.Hash) { - Write-Output "rustdesk_printer_driver_v4, checksums do not match, ignore the file." + Write-Output "rustdesk_printer_driver_v4-1.4, checksums do not match, ignore the file." } else { Write-Output "printer_driver_adapter.dll, checksums do not match, ignore the file." } From 9db7217cabfd11fecd3ae89d98b5e98fc8165e1f Mon Sep 17 00:00:00 2001 From: Lynilia <89228568+Lynilia@users.noreply.github.com> Date: Mon, 28 Jul 2025 06:12:44 +0200 Subject: [PATCH 453/506] Update fr.rs (#12438) --- src/lang/fr.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 3ca48a25853..9b9af2bb456 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -702,13 +702,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Activer le terminal"), ("New tab", "Nouvel onglet"), ("Keep terminal sessions on disconnect", "Maintenir les sessions du terminal lors de la déconnexion"), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), - ("elevation_username_tip", ""), + ("Terminal (Run as administrator)", "Terminal (administrateur)"), + ("terminal-admin-login-tip", "Veuillez saisir le nom d’utilisateur et le mot de passe de l’administrateur de l’appareil contrôlé."), + ("Failed to get user token.", "Échec de l’obtention du jeton utilisateur."), + ("Incorrect username or password.", "Nom d’utilisateur ou mot de passe incorrect."), + ("The user is not an administrator.", "L’utilisateur n’est pas un administrateur."), + ("Failed to check if the user is an administrator.", "Échec de la vérification du statut d’administrateur de l’utilisateur."), + ("Supported only in the installed version.", "Uniquement pris en charge dans la version installée."), + ("elevation_username_tip", "Saisissez un nom d’utilisateur ou un domaine\\utilisateur"), ].iter().cloned().collect(); } From af53b1e8c95dea8ca6710b572564e7f3662aa2fe Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:14:07 +0800 Subject: [PATCH 454/506] fix: rendezvous server timeout (#12443) Signed-off-by: fufesou --- src/rendezvous_mediator.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 54a3362d10a..8db5f8f5f89 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -2,7 +2,7 @@ use std::{ net::SocketAddr, sync::{ atomic::{AtomicBool, Ordering}, - Arc, + Arc, RwLock, }, time::{Duration, Instant}, }; @@ -93,7 +93,7 @@ impl RendezvousMediator { } scrap::codec::test_av1(); loop { - let mut timeout = CONNECT_TIMEOUT; + let timeout = Arc::new(RwLock::new(CONNECT_TIMEOUT)); let conn_start_time = Instant::now(); *SOLVING_PK_MISMATCH.lock().await = "".to_owned(); if !config::option2bool("stop-service", &Config::get_option("stop-service")) @@ -105,6 +105,7 @@ impl RendezvousMediator { MANUAL_RESTARTED.store(false, Ordering::SeqCst); for host in servers.clone() { let server = server.clone(); + let timeout = timeout.clone(); futs.push(tokio::spawn(async move { if let Err(err) = Self::start(server, host).await { let err = format!("rendezvous mediator error: {err}"); @@ -113,7 +114,7 @@ impl RendezvousMediator { if err.contains("10054") || err.contains("11001") { // No such host is known. (os error 11001) // An existing connection was forcibly closed by the remote host. (os error 10054): also happens for UDP - timeout = 3000; + *timeout.write().unwrap() = 3000; } log::error!("{err}"); } @@ -126,9 +127,10 @@ impl RendezvousMediator { server.write().unwrap().close_connections(); } Config::reset_online(); + let timeout = *timeout.read().unwrap(); if !MANUAL_RESTARTED.load(Ordering::SeqCst) { let elapsed = conn_start_time.elapsed().as_millis() as u64; - if elapsed < timeout{ + if elapsed < timeout { sleep(((timeout - elapsed) / 1000) as _).await; } } else { From 7a3e67e1d3438845577aac703375aecfc865ea20 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 28 Jul 2025 20:06:30 +0800 Subject: [PATCH 455/506] fix connect timeout of udp_nat_connect and udp_nat_listen (#12447) Signed-off-by: 21pages --- src/client.rs | 10 ++++++---- src/rendezvous_mediator.rs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index 073bf53ffd6..e13ccff1141 100644 --- a/src/client.rs +++ b/src/client.rs @@ -447,7 +447,8 @@ impl Client { let addr = AddrMangle::decode(&rr.socket_addr_v6); if addr.port() > 0 { if s.connect(addr).await.is_ok() { - connect_futures.push(udp_nat_connect(s, "IPv6").boxed()); + connect_futures + .push(udp_nat_connect(s, "IPv6", CONNECT_TIMEOUT).boxed()); } } } @@ -589,10 +590,10 @@ impl Client { .boxed(), ); if let Some(udp_socket_nat) = udp_socket_nat { - connect_futures.push(udp_nat_connect(udp_socket_nat, "UDP").boxed()); + connect_futures.push(udp_nat_connect(udp_socket_nat, "UDP", connect_timeout).boxed()); } if let Some(udp_socket_v6) = udp_socket_v6 { - connect_futures.push(udp_nat_connect(udp_socket_v6, "IPv6").boxed()); + connect_futures.push(udp_nat_connect(udp_socket_v6, "IPv6", connect_timeout).boxed()); } // Run all connection attempts concurrently, return the first successful one let (mut conn, kcp, mut typ) = match select_ok(connect_futures).await { @@ -4009,6 +4010,7 @@ async fn test_udp_uat( async fn udp_nat_connect( socket: Arc, typ: &'static str, + ms_timeout: u64, ) -> ResultType<(Stream, Option, &'static str)> { crate::punch_udp(socket.clone(), false) .await @@ -4016,7 +4018,7 @@ async fn udp_nat_connect( log::debug!("{err}"); anyhow!(err) })?; - let res = KcpStream::connect(socket, Duration::from_secs(CONNECT_TIMEOUT as _)) + let res = KcpStream::connect(socket, Duration::from_millis(ms_timeout)) .await .map_err(|err| { log::debug!("Failed to connect KCP stream: {}", err); diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 8db5f8f5f89..e17920c8a10 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -834,7 +834,7 @@ async fn udp_nat_listen( let res = crate::punch_udp(socket.clone(), true).await?; let stream = crate::kcp_stream::KcpStream::accept( socket, - Duration::from_secs(CONNECT_TIMEOUT as _), + Duration::from_millis(CONNECT_TIMEOUT as _), res, ) .await?; From 26e5f7bbebc282fec2ef709b0fc54de478d2abd5 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 29 Jul 2025 11:53:45 +0800 Subject: [PATCH 456/506] show websocket option on desktop --- flutter/lib/desktop/pages/desktop_setting_page.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 9d182f9f895..cc1f3f27194 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1522,9 +1522,8 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; final hideProxy = isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; - // final hideWebSocket = isWeb || - // bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y'; - final hideWebSocket = true; + final hideWebSocket = isWeb || + bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y'; if (hideServer && hideProxy && hideWebSocket) { return Offstage(); From d9674a2d772a29e6078d53509ee38e7f3eaf439f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:03:07 +0800 Subject: [PATCH 457/506] Git submodule: Bump libs/hbb_common from `f91459c` to `57c8a23` (#12459) Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `f91459c` to `57c8a23`. - [Release notes](https://github.com/rustdesk/hbb_common/releases) - [Commits](https://github.com/rustdesk/hbb_common/compare/f91459c4ab80fc3cfdef0882b2af51f984bc914c...57c8a23ab970587ea6380943b04dc354020bbe7c) --- updated-dependencies: - dependency-name: libs/hbb_common dependency-version: 57c8a23ab970587ea6380943b04dc354020bbe7c dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index f91459c4ab8..57c8a23ab97 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit f91459c4ab80fc3cfdef0882b2af51f984bc914c +Subproject commit 57c8a23ab970587ea6380943b04dc354020bbe7c From d55b98b18707829d1d1d5124a05f430c8a5075f6 Mon Sep 17 00:00:00 2001 From: Mahdi Rahimi <31624047+mahdirahimi1999@users.noreply.github.com> Date: Wed, 30 Jul 2025 08:43:28 +0330 Subject: [PATCH 458/506] Updated Persian translations in fa.rs (#12450) --- src/lang/fa.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 073f406a68a..558936efb43 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -702,13 +702,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "فعال‌سازی ترمینال"), ("New tab", "زبانه جدید"), ("Keep terminal sessions on disconnect", "حفظ جلسات ترمینال پس از قطع اتصال"), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), - ("elevation_username_tip", ""), + ("Terminal (Run as administrator)", "ترمینال (اجرای به عنوان مدیر سیستم)"), + ("terminal-admin-login-tip", "برای اجرای ترمینال به‌عنوان مدیر، نام کاربری و رمز عبور مدیر سیستم را وارد کنید."), + ("Failed to get user token.", "دریافت توکن کاربر ناموفق بود."), + ("Incorrect username or password.", "نام کاربری یا رمز عبور اشتباه است."), + ("The user is not an administrator.", "کاربر دارای دسترسی مدیر سیستم نیست."), + ("Failed to check if the user is an administrator.", "بررسی وضعیت مدیر سیستم برای کاربر ناموفق بود."), + ("Supported only in the installed version.", "فقط در نسخه نصب‌شده پشتیبانی می‌شود."), + ("elevation_username_tip", "لطفاً نام کاربری مدیریتی را برای ارتقاء دسترسی وارد کنید."), ].iter().cloned().collect(); } From 7ece7e730a73ace442eab27d84babd15d910088a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 30 Jul 2025 21:05:46 +0800 Subject: [PATCH 459/506] fix https://github.com/rustdesk/rustdesk/issues/12481 --- 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/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/ge.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/sc.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/ta.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/vi.rs | 2 -- 47 files changed, 1 insertion(+), 93 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 0f302d8e148..b975e9c6421 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -434,7 +434,7 @@ class _DesktopHomePageState extends State !isCardClosed && bind.mainUriPrefixSync().contains('rustdesk')) { final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled(); - String btnText = isToUpdate ? 'Click to update' : 'Click to download'; + String btnText = isToUpdate ? 'Update' : "Download'; GestureTapCallback onPressed = () 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 7ba1d35df2c..edc9d7e98ac 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "كلمة مرور نظام التشغيل"), ("install_tip", "بسبب صلاحيات تحكم حساب المستخدم. RustDesk قد لا يعمل بشكل صحيح في جهة البعيد في بعض الحالات. لتفادي ذلك. الرجاء الضغط على الزر ادناه لتثبيت RustDesk في جهازك."), ("Click to upgrade", "اضغط للارتقاء"), - ("Click to download", "اضغط للتنزيل"), - ("Click to update", "ضغط للتحديث"), ("Configure", "تهيئة"), ("config_acc", "لتتمكن من التحكم بسطح مكتبك البعيد, تحتاج الى منح RustDesk اذونات \"امكانية الوصول\"."), ("config_screen", "لتتمكن من الوصول الى سطح مكتبك البعيد, تحتاج الى منح RustDesk اذونات \"تسجيل الشاشة\"."), diff --git a/src/lang/be.rs b/src/lang/be.rs index d2254749257..a57802bf11d 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Пароль ўваходу ў аперацыйную сістэму"), ("install_tip", "У некаторых выпадках RustDesk можа працаваць няправільна на аддаленым вузле з-за UAC. Каб пазбегнуць магчымых праблем з UAC, націсніце кнопку ніжэй для ўстаноўкі RustDesk у сістэме."), ("Click to upgrade", "Абнавіць"), - ("Click to download", "Спампаваць"), - ("Click to update", "Абнавіць"), ("Configure", "Наладзіць"), ("config_acc", "Каб аддаленна кіраваць сваім працоўным сталом, вам неабходна дазволіць RustDesk правы доступу."), ("config_screen", "Для аддаленага доступу да працоўнага сталу вам неабходна дазволіць RustDesk правы здымку экрана."), diff --git a/src/lang/bg.rs b/src/lang/bg.rs index ffaf66aa984..9988ead28bf 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -147,8 +147,6 @@ 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 update", "Натиснете, за да обновите"), ("Configure", "Настройване"), ("config_acc", "За да управлявате вашия работна среда отдалечено, трябва да предоставите на RustDesk права от раздел \"Достъпност\"."), ("config_screen", "За да управлявате вашия работна среда отдалечено, трябва да предоставите на RustDesk права от раздел \"Запис на екрана\"."), diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 772a0baa78d..fc228bb8b3a 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index be4321419ae..18fba8d5483 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "操作系统密码"), ("install_tip", "你正在运行未安装版本,由于 UAC 限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将 RustDesk 安装到系统,从而规避上述问题。"), ("Click to upgrade", "点击这里升级"), - ("Click to download", "点击这里下载"), - ("Click to update", "点击这里更新"), ("Configure", "配置"), ("config_acc", "为了能够远程控制你的桌面, 请给予 RustDesk \"辅助功能\" 权限。"), ("config_screen", "为了能够远程访问你的桌面, 请给予 RustDesk \"屏幕录制\" 权限。"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 3f1c0b753fd..30aac8df667 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Heslo do operačního systému"), ("install_tip", "Kvůli řízení oprávnění v systému (UAC), RustDesk v některých případech na protistraně nefunguje správně. Abyste se UAC vyhnuli, klikněte na níže uvedené tlačítko a nainstalujte tak RustDesk do systému."), ("Click to upgrade", "Aktualizovat"), - ("Click to download", "Stáhnout"), - ("Click to update", "Aktualizovat"), ("Configure", "Nastavit"), ("config_acc", "Aby bylo možné na dálku ovládat vaši plochu, je třeba aplikaci RustDesk udělit oprávnění pro \"Zpřístupnění pro hendikepované\"."), ("config_screen", "Aby bylo možné přistupovat k vaší ploše na dálku, je třeba aplikaci RustDesk udělit oprávnění pro \"Nahrávání obsahu obrazovky\"."), diff --git a/src/lang/da.rs b/src/lang/da.rs index f3e212eec3b..7870767e428 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Operativsystemadgangskode"), ("install_tip", "På grund af UAC kan RustDesk ikke fungere korrekt i nogle tilfælde på fjernskrivebordet. For at undgå UAC skal du klikke på knappen nedenfor for at installere RustDesk på systemet"), ("Click to upgrade", "Klik for at opgradere"), - ("Click to download", "Klik for at downloade"), - ("Click to update", "Klik for at opdatere"), ("Configure", "Konfigurer"), ("config_acc", "For at kontrollere dit skrivebord på afstand skal du give RustDesk \"Access \" Rettigheder."), ("config_screen", "For at kunne få adgang til dit skrivebord langtfra, skal du give RustDesk \"skærmstøtte \" tilladelser."), diff --git a/src/lang/de.rs b/src/lang/de.rs index f2ffe79bae1..05e145c2255 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Betriebssystem-Passwort"), ("install_tip", "Aufgrund der Benutzerkontensteuerung (UAC) kann RustDesk in manchen Fällen nicht ordnungsgemäß funktionieren. Um die Benutzerkontensteuerung zu umgehen, klicken Sie bitte auf die Schaltfläche unten und installieren RustDesk auf dem System."), ("Click to upgrade", "Zum Upgraden klicken"), - ("Click to download", "Zum Herunterladen klicken"), - ("Click to update", "Zum Aktualisieren klicken"), ("Configure", "Konfigurieren"), ("config_acc", "Um Ihren PC aus der Ferne zu steuern, müssen Sie RustDesk Zugriffsrechte erteilen."), ("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk die Berechtigung \"Bildschirmaufnahme\" erteilen."), diff --git a/src/lang/el.rs b/src/lang/el.rs index 9f5f7be0c3d..22418bb007f 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -147,8 +147,6 @@ 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 update", "Ενημέρωση τώρα"), ("Configure", "Διαμόρφωση"), ("config_acc", "Για τον απομακρυσμένο έλεγχο του υπολογιστή σας, πρέπει να εκχωρήσετε δικαιώματα πρόσβασης στο RustDesk."), ("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στον υπολογιστή σας, πρέπει να εκχωρήσετε το δικαίωμα RustDesk \"Screen Capture\"."), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 912faa74483..4ef2476f215 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Pasvorto de la operaciumo"), ("install_tip", "Vi ne uzas instalita versio. Pro limigoj pro UAC, kiel aparato kontrolata, en kelkaj kazoj, ne estos ebla kontroli la muson kaj klavaron aŭ registri la ekranon. Bonvolu alkliku la butonon malsupre por instali RustDesk sur la operaciumo por eviti la demando supre."), ("Click to upgrade", "Alklaki por plibonigi"), - ("Click to download", "Alklaki por elŝuti"), - ("Click to update", "Alklaki por ĝisdatigi"), ("Configure", "Konfiguri"), ("config_acc", "Por uzi vian foran aparaton, bonvolu doni la permeson \"alirebleco\" al RustDesk."), ("config_screen", "Por uzi vian foran aparaton, bonvolu doni la permeson \"ekranregistrado\" al RustDesk."), diff --git a/src/lang/es.rs b/src/lang/es.rs index 1365efa5875..35297ca78d4 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Contraseña del sistema operativo"), ("install_tip", "Debido al Control de cuentas de usuario, es posible que RustDesk no funcione correctamente como escritorio remoto. Para evitar este problema, haga clic en el botón de abajo para instalar RustDesk a nivel de sistema."), ("Click to upgrade", "Clic para actualizar"), - ("Click to download", "Clic para descargar"), - ("Click to update", "Clic para refrescar"), ("Configure", "Configurar"), ("config_acc", "Para controlar su escritorio desde el exterior, debe otorgar permiso a RustDesk de \"Accesibilidad\"."), ("config_screen", "Para controlar su escritorio desde el exterior, debe otorgar permiso a RustDesk de \"Grabación de pantalla\"."), diff --git a/src/lang/et.rs b/src/lang/et.rs index d8ac432819b..70cd9267b73 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Opsüsteemi parool"), ("install_tip", "Kasutajakonto kontrolli (UAC) tõttu ei saa RustDesk mõnel juhul korralikult kaugjuhtimispoolena töötada. Kontrolli vältimiseks palun klõpsa alloleval nupul, et RustDesk oma süsteemi paigaldada."), ("Click to upgrade", "Vajuta täiendamiseks"), - ("Click to download", "Vajuta allalaadimiseks"), - ("Click to update", "Vajuta uuendamiseks"), ("Configure", "Seadista"), ("config_acc", "Töölaua kaugjuhtimiseks tuleb RustDeskile anda \"juurdepääsetavuse\" õigused."), ("config_screen", "Töölaua kaugjuhtimiseks tuleb RustDeskile anda \"ekraanisalvestuse\" õigused."), diff --git a/src/lang/eu.rs b/src/lang/eu.rs index d50291fde7c..914a4eb62bc 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Sistema eragilearen pasahitza"), ("install_tip", "Erabiltzaile Kontuen Kontrolarengatik, RustDesk ezin du ondo funtzionatu urruneko mahaigainean. EKK saihesteko, mesedez, egin klik azpiko botoian RustDesk sistema mailan instalatzeko."), ("Click to upgrade", "Egin klik bertsio-berritzeko"), - ("Click to download", "Egin klik deskargatzeko"), - ("Click to update", "Egin klik eguneratzeko"), ("Configure", "Konfiguratu"), ("config_acc", "Zure mahaigaina urrunetik kontrolatzeko, RustDesk-i \"Irisgarritasuna\" baimenak eman behar dituzu."), ("config_screen", "Zure mahaigaina kanpotik kontrolatzeko, RustDesk-i \"Pantaila grabatu\" baimena eman behar duzu."), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 558936efb43..a68951ff25b 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "رمز عبور سیستم عامل"), ("install_tip", "لطفا برنامه را نصب کنید UAC و جلوگیری از خطای RustDesk برای راحتی در استفاده از نرم افزار"), ("Click to upgrade", "برای ارتقا کلیک کنید"), - ("Click to download", "برای دانلود کلیک کنید"), - ("Click to update", "برای به روز رسانی کلیک کنید"), ("Configure", "تنظیم"), ("config_acc", "بدهید \"access\" مجوز RustDesk برای کنترل از راه دور دسکتاپ باید به"), ("config_screen", "بدهید \"screenshot\" مجوز RustDesk برای کنترل از راه دور دسکتاپ باید به"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9b9af2bb456..b66d1c2c241 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Mot de passe du système d’exploitation"), ("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", "Mettre à niveau"), - ("Click to download", "Télécharger"), - ("Click to update", "Mettre à jour"), ("Configure", "Configurer"), ("config_acc", "L’autorisation « Accessibilité » est requise pour contrôler votre bureau à distance."), ("config_screen", "L’autorisation « Enregistrement d’écran » est requise pour accéder à votre bureau à distance."), diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 3e72cc96fdc..db2be1836a2 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -147,8 +147,6 @@ 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 update", "დააჭირეთ განახლებისთვის"), ("Configure", "კონფიგურაცია"), ("config_acc", "თქვენი სამუშაო მაგიდის დისტანციური მართვისთვის უნდა მიანიჭოთ RustDesk-ს \"წვდომის\" უფლებები"), ("config_screen", "სამუშაო მაგიდაზე დისტანციური წვდომისთვის უნდა მიანიჭოთ RustDesk-ს \"ეკრანის ანაბეჭდის\" უფლებები"), diff --git a/src/lang/he.rs b/src/lang/he.rs index 3d0efd5b619..e4299b00945 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -147,8 +147,6 @@ 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 update", "לחץ כדי לעדכן"), ("Configure", "הגדר"), ("config_acc", "כדי לשלוט מרחוק בשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"נגישות\"."), ("config_screen", "כדי לגשת מרחוק לשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"הקלטת מסך\"."), diff --git a/src/lang/hr.rs b/src/lang/hr.rs index f04e2c10ae6..f3a900acee4 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Lozinka OS-a"), ("install_tip", "Zbog UAC-a RustDesk ne može u nekim slučajevima raditi pravilno. Da biste prevazišli UAC, kliknite na tipku ispod da instalirate RustDesk na sustav."), ("Click to upgrade", "Klik za nadogradnju"), - ("Click to download", "Klik za preuzimanje"), - ("Click to update", "Klik za ažuriranje"), ("Configure", "Konfiguracija"), ("config_acc", "Da biste daljinski kontrolirali radnu površinu, RustDesk-u trebate dodijeliti prava za \"Pristupačnost\"."), ("config_screen", "Da biste daljinski pristupili radnoj površini, RustDesk-u trebate dodijeliti prava za \"Snimanje zaslona\"."), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index fcb1958721b..daedab9c771 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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álatakor. 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 számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell biztosítania."), ("config_screen", "Ahhoz, hogy távolról hozzáférhessen számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."), diff --git a/src/lang/id.rs b/src/lang/id.rs index 9ecaaeb3b8f..6718a719af7 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Kata Sandi OS"), ("install_tip", "Karena UAC, RustDesk tidak dapat bekerja dengan baik sebagai sisi remote dalam beberapa kasus. Untuk menghindari UAC, silakan klik tombol di bawah ini untuk menginstal RustDesk ke sistem."), ("Click to upgrade", "Klik untuk upgrade"), - ("Click to download", "Klik untuk unduh"), - ("Click to update", "Klik untuk memperbarui"), ("Configure", "Konfigurasi"), ("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."), diff --git a/src/lang/it.rs b/src/lang/it.rs index fcebe35b45f..f52a1be2ba4 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Password sistema operativo"), ("install_tip", "A causa del Controllo Account Utente (UAC), RustDesk potrebbe non funzionare correttamente come desktop remoto.\nPer evitare questo problema, fai clic sul tasto qui sotto per installare RustDesk a livello di sistema."), ("Click to upgrade", "Aggiorna"), - ("Click to download", "Download"), - ("Click to update", "Aggiorna"), ("Configure", "Configura"), ("config_acc", "Per controllare il desktop dall'esterno, devi fornire a RustDesk il permesso 'Accessibilità'."), ("config_screen", "Per controllare il desktop dall'esterno, devi fornire a RustDesk il permesso 'Registrazione schermo'."), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 9e0852afa95..597f43c1ffc 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OSのパスワード"), ("install_tip", "UACの影響により、RustDeskがリモートコンピューター上で正常に動作しない場合があります。UACを回避するには、下のボタンをクリックしてシステムにRustDeskをインストールしてください。"), ("Click to upgrade", "アップグレード"), - ("Click to download", "ダウンロード"), - ("Click to update", "アップデート"), ("Configure", "設定"), ("config_acc", "リモートからあなたのコンピューターを操作するには、RustDeskに「アクセシビリティ」権限を与える必要があります。"), ("config_screen", "リモートからあなたのコンピューターにアクセスするには、RustDeskに「画面録画」の権限を与える必要があります。"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 91e6fda7096..ef918844e7b 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OS 비밀번호"), ("install_tip", "UAC로 인해 경우에 따라 RustDesk가 원격 쪽에서 제대로 작동하지 않을 수 있습니다. UAC를 피하려면 아래 버튼을 클릭하여 시스템에 RustDesk를 설치하세요."), ("Click to upgrade", "업그레이드하려면 클릭"), - ("Click to download", "다운로드하려면 클릭"), - ("Click to update", "업데이트하려면 클릭"), ("Configure", "구성"), ("config_acc", "데스크톱을 원격으로 제어하려면 RustDesk에 \"접근성\" 권한을 부여해야 합니다."), ("config_screen", "데스크톱에 원격으로 액세스하려면 RustDesk에 \"화면 녹화\" 권한을 부여해야 합니다."), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 6c9bb7a498f..467d3fcea1b 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OS Құпия сөзі"), ("install_tip", "UAC кесірінен, RustDesk кейбірде қашықтағы жақ ретінде дұрыс жұмыс істей алмайды. UAC'пен қиындықты болдырмау үшін, төмендегі батырманы басып RustDesk'ті жүйеге орнатыңыз."), ("Click to upgrade", "Жаңғырту үшін басыңыз"), - ("Click to download", "Жүктеу үшін басыңыз"), - ("Click to update", "Жаңарту үшін басыңыз"), ("Configure", "Қалыптау"), ("config_acc", "Сіздің Жұмыс үстеліңізді қашықтан басқару үшін, RustDesk'ке \"Қолжетімділік\" рұқсаттарын беруіңіз керек."), ("config_screen", "Сіздің Жұмыс үстеліңізге қашықтан қол жеткізу үшін, RustDesk'ке \"Екіренді Жазу\" рұқсаттарын беруіңіз керек."), diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 1cecafb7263..a35ff066061 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OS slaptažodis"), ("install_tip", "Kai kuriais atvejais UAC gali priversti RustDesk netinkamai veikti nuotoliniame pagrindiniame kompiuteryje. Norėdami apeiti UAC, spustelėkite toliau esantį mygtuką, kad įdiegtumėte RustDesk į savo kompiuterį."), ("Click to upgrade", "Spustelėkite, jei norite atnaujinti"), - ("Click to download", "Spustelėkite norėdami atsisiųsti"), - ("Click to update", "Spustelėkite norėdami atnaujinti"), ("Configure", "Konfigūruoti"), ("config_acc", "Norėdami nuotoliniu būdu valdyti darbalaukį, turite suteikti RustDesk \"prieigos\" leidimus"), ("config_screen", "Norėdami nuotoliniu būdu pasiekti darbalaukį, turite suteikti RustDesk leidimus \"ekrano kopija\""), diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 3b1e0a2de8f..126ad075e3e 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OS parole"), ("install_tip", "UAC dēļ RustDesk dažos gadījumos nevar pareizi darboties kā attālā puse. Lai izvairītos no UAC, lūdzu, noklikšķiniet uz tālāk esošās pogas, lai instalētu RustDesk sistēmā."), ("Click to upgrade", "Jaunināt"), - ("Click to download", "Lejupielādēt"), - ("Click to update", "Atjaunināt"), ("Configure", "Konfigurēt"), ("config_acc", "Lai attālināti vadītu savu darbvirsmu, jums ir jāpiešķir RustDesk \"Pieejamība\" atļaujas."), ("config_screen", "Lai attālināti piekļūtu darbvirsmai, jums ir jāpiešķir RustDesk \"Ekrāna tveršana\" atļaujas."), diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 3b69f9a1f7a..cec6ec3a183 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Operativsystempassord"), ("install_tip", "På grunn av UAC kan RustDesk ikke fungere korrekt i enkelte tillfeller på fjernskrivebordet. For å unngå UAC klikker du på knappen nedenfor for å installere RustDesk på systemet"), ("Click to upgrade", "Klikk for å oppgradere"), - ("Click to download", "Klikk for å laste ned"), - ("Click to update", "Klikk for å oppdatere"), ("Configure", "Konfigurer"), ("config_acc", "For å kontrollere ditt skrivebord med fjernstyring må du gi RustDesk \"Access \" Rettigheter."), ("config_screen", "For å kunne få adgang til ditt skrivebord med fjernstyring, må du gi RustDesk \"skjerstøtte \" tillatelser."), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 025a8f22cb6..0b352a6241e 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OS Wachtwoord"), ("install_tip", "Door UAC-beperkingen lukt het niet altijd om uw bureaublad op afstand te bedienen. Installeer RustDesk op het systeem om dit probleem te voorkomen."), ("Click to upgrade", "Klik voor upgrade"), - ("Click to download", "Klik om te downloaden"), - ("Click to update", "Klik om bij te werken"), ("Configure", "Configureren"), ("config_acc", "Om uw apparaat op afstand te kunnen bedienen, moet u RustDesk toestemming voor Toegankelijkheid geven."), ("config_screen", "Om uw apparaat op afstand te kunnen bedienen, moet u RustDesk toestemming voor Schermopname geven."), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 913db786467..97befe1eeae 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Hasło systemu operacyjnego"), ("install_tip", "RustDesk może nie działać poprawnie na maszynie zdalnej z przyczyn związanych z UAC. W celu uniknięcia problemów z UAC, kliknij poniższy przycisk by zainstalować RustDesk w swoim systemie."), ("Click to upgrade", "Zaktualizuj"), - ("Click to download", "Pobierz"), - ("Click to update", "Uaktualnij"), ("Configure", "Konfiguruj"), ("config_acc", "Konfiguracja konta"), ("config_screen", "Konfiguracja ekranu"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 1012f26954f..cbb9aa7c93a 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Senha do SO"), ("install_tip", "Devido ao UAC, o RustDesk não funciona correctamente em alguns casos. Para evitar o UAC, por favor clique no botão abaixo para instalar o RustDesk no sistema."), ("Click to upgrade", "Clique para atualizar"), - ("Click to download", "Clique para carregar"), - ("Click to update", "Clique para fazer a actualização"), ("Configure", "Configurar"), ("config_acc", "Para controlar o seu Ambiente de Trabalho remotamente, é preciso conceder ao RustDesk permissões de \"Acessibilidade\"."), ("config_screen", "Para aceder ao seu Ambiente de Trabalho remotamente, é preciso conceder ao RustDesk permissões de \"Gravar a Tela\"/"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 9ed8328ce6f..7058fd7b1f1 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Senha do SO"), ("install_tip", "Devido ao UAC, o RustDesk não funciona corretamente como o lado remoto em alguns casos. Para evitar o UAC, por favor clique no botão abaixo para instalar o RustDesk no sistema."), ("Click to upgrade", "Clique para fazer o upgrade"), - ("Click to download", "Clique para baixar"), - ("Click to update", "Clique para fazer o update"), ("Configure", "Configurar"), ("config_acc", "Para controlar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Acessibilidade\"."), ("config_screen", "Para acessar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Gravar a Tela\"/"), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index ad44894c16f..ce912cb35c5 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Parolă sistem"), ("install_tip", "Din cauza restricțiilor CCU, este posibil ca RustDesk să nu funcționeze corespunzător. Pentru a evita acest lucru, dă clic pe butonul de mai jos pentru a instala RustDesk."), ("Click to upgrade", "Dă clic pentru a face upgrade"), - ("Click to download", "Dă clic pentru a descărca"), - ("Click to update", "Dă clic pentru a actualiza"), ("Configure", "Configurează"), ("config_acc", "Pentru a controla desktopul la distanță, trebuie să permiți RustDesk acces la setările de Accesibilitate."), ("config_screen", "Pentru a controla desktopul la distanță, trebuie să permiți RustDesk acces la setările de Înregistrare ecran."), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index e40c93a7e37..c200cf77481 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -147,8 +147,6 @@ 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 update", "Нажмите, чтобы обновить"), ("Configure", "Настроить"), ("config_acc", "Чтобы удалённо управлять своим рабочим столом, вы должны предоставить RustDesk права \"доступа\""), ("config_screen", "Для удалённого доступа к рабочему столу вы должны предоставить RustDesk права \"снимок экрана\""), diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 525f3fc4f51..3649e4519b8 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Crae sistema operativu"), ("install_tip", "Pro neghe de su Controllu Contu Utente (UAC), RustDesk diat pòdere non funtzionare comente si tocat comente iscrivania remota.\nPro evitare custu problema, incarca in su butone inoghe in suta pro installare RustDesk a livellu de sistema."), ("Click to upgrade", "Atualiza"), - ("Click to download", "Iscàrriga"), - ("Click to update", "Annoa"), ("Configure", "Cunfigura"), ("config_acc", "Pro controllare s'iscrivania dae foras, depes frunire a RustDesk su permissu 'Atzessibilidade'."), ("config_screen", "Pro controllare s'iscrivania dae foras, depes frunire a RustDesk su permissu 'Registratzione ischermu'."), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 9e935554fbc..580486e8519 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Heslo do operačného systému"), ("install_tip", "V niektorých prípadoch RustDesk nefunguje správne z dôvodu riadenia užívateľských oprávnení (UAC). Vyhnete sa tomu kliknutím na nižšie zobrazene tlačítko a nainštalovaním RuskDesk do systému."), ("Click to upgrade", "Kliknutím nainštalujete aktualizáciu"), - ("Click to download", "Kliknutím potvrďte stiahnutie"), - ("Click to update", "Kliknutím aktualizovať"), ("Configure", "Nastaviť"), ("config_acc", "Aby bolo možné na diaľku ovládať vašu plochu, je potrebné aplikácii RustDesk udeliť práva \"Dostupnosť\"."), ("config_screen", "Aby bolo možné na diaľku sledovať vašu obrazovku, je potrebné aplikácii RustDesk udeliť práva \"Zachytávanie obsahu obrazovky\"."), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index c81150f20d0..2edcf26ce63 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Geslo operacijskega sistema"), ("install_tip", "Zaradi nadzora uporabniškega računa, RustDesk v nekaterih primerih na oddaljeni strani ne deluje pravilno. Temu se lahko izognete z namestitvijo."), ("Click to upgrade", "Klikni za nadgradnjo"), - ("Click to download", "Klikni za prenos"), - ("Click to update", "Klikni za posodobitev"), ("Configure", "Nastavi"), ("config_acc", "Za oddaljeni nadzor namizja morate RustDesku dodeliti pravico za dostopnost"), ("config_screen", "Za oddaljeni dostop do namizja morate RustDesku dodeliti pravico snemanje zaslona"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 52ccf2d9722..a054314d970 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OS fjalëkalim"), ("install_tip", "Për shkak të UAC, RustDesk nuk mund të punoj sic duhet si nje remote në distancë në disa raste. Për të shamngur UAC, ju lutem klikoni butonin më poshtë për të instaluar RustDesk në sistem."), ("Click to upgrade", "Klikoni për përmirësim"), - ("Click to download", "Klikoni për tu shkarkuar"), - ("Click to update", "Klikoni për përditësim"), ("Configure", "Koniguro"), ("config_acc", "Për të kontrolluar Desktopin tuaj nga distanca, duhet të jepni leje RustDesk \"Aksesueshmëri\"."), ("config_screen", "Për të aksesuar Desktopin tuaj nga distanca, duhet ti jepni lejet RustDesk \"Regjistrimin e ekranit\"."), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index d60b21ba7c7..a8bc4e0d350 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OS lozinka"), ("install_tip", "Zbog UAC RustDesk ne može raditi pravilno u nekim slučajevima. Da biste prevazišli UAC, kliknite taster ispod da instalirate RustDesk na sistem."), ("Click to upgrade", "Klik za nadogradnju"), - ("Click to download", "Klik za preuzimanje"), - ("Click to update", "Klik za ažuriranje"), ("Configure", "Konfigurisanje"), ("config_acc", "Da biste daljinski kontrolisali radnu površinu, RustDesk-u treba da dodelite \"Accessibility\" prava."), ("config_screen", "Da biste daljinski pristupili radnoj površini, RustDesk-u treba da dodelite \"Screen Recording\" prava."), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 356ef13ade9..f394b90a390 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OS lösenord"), ("install_tip", "På grund av UAC, kan inte RustDesk fungera ordentligt på klientsidan. För att undvika problem med UAC, tryck på knappen nedan för att installera RustDesk på systemet."), ("Click to upgrade", "Klicka för att nedgradera"), - ("Click to download", "Klicka för att ladda ner"), - ("Click to update", "Klicka för att uppdatera"), ("Configure", "Konfigurera"), ("config_acc", "För att kontrollera din dator på distans måste du ge RustDesk \"Tillgänglighets\" rättigheter."), ("config_screen", "För att kontrollera din dator på distans måste du ge RustDesk \"Skärminspelnings\" rättigheter."), diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 29f3e79145f..b925e40db1a 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OS கடவுச்சொல்"), ("install_tip", "நிறுவு_குறிப்பு"), ("Click to upgrade", "மேம்படுத்த கிளிக் செய்"), - ("Click to download", "பதிவிறக்க கிளிக் செய்"), - ("Click to update", "புதுப்பிக்க கிளிக் செய்"), ("Configure", "உள்ளமை"), ("config_acc", "உள்ளமைவு_அக்கெஸ்ஸ்"), ("config_screen", "config_screen"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 5407634898c..8b311303b0b 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", ""), ("install_tip", ""), ("Click to upgrade", ""), - ("Click to download", ""), - ("Click to update", ""), ("Configure", ""), ("config_acc", ""), ("config_screen", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index d64931aec39..a0fd34042ef 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -147,8 +147,6 @@ 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 update", "คลิกเพื่ออัปเดต"), ("Configure", "ปรับแต่งค่า"), ("config_acc", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่ RustDesk"), ("config_screen", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การบันทึกภาพหน้าจอ\" ให้แก่ RustDesk"), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f2af97fe3aa..cff87d8555e 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "İşletim Sistemi Şifresi"), ("install_tip", "Kullanıcı Hesabı Denetimi nedeniyle, RustDesk bir uzak masaüstü olarak düzgün çalışmayabilir. Bu sorunu önlemek için, RustDesk'i sistem seviyesinde kurmak için aşağıdaki butona tıklayın."), ("Click to upgrade", "Yükseltmek için tıklayınız"), - ("Click to download", "İndirmek için tıklayınız"), - ("Click to update", "Güncellemek için tıklayınız"), ("Configure", "Ayarla"), ("config_acc", "Masaüstünüzü dışarıdan kontrol etmek için RustDesk'e \"Erişilebilirlik\""), ("config_screen", "Masaüstünüzü dışarıdan kontrol etmek için RustDesk'e \"Ekran Kaydı\" iznini vermeniz gerekir."), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 9e2703c83ec..7e75af13fda 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -147,8 +147,6 @@ 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 update", "點選以更新"), ("Configure", "設定"), ("config_acc", "為了遠端控制您的桌面,您需要授予 RustDesk「無障礙功能」權限。"), ("config_screen", "為了遠端存取您的桌面,您需要授予 RustDesk「螢幕錄製」權限。"), diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 8c4ab0bb1ae..98afe623833 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -147,8 +147,6 @@ 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 update", "Натисніть, щоб оновити"), ("Configure", "Налаштувати"), ("config_acc", "Для віддаленого керування вашою стільницею, вам необхідно надати RustDesk дозволи \"Спеціальні можливості\""), ("config_screen", "Для віддаленого доступу до вашої стільниці, вам необхідно надати RustDesk дозволи на \"Запис екрана\""), diff --git a/src/lang/vi.rs b/src/lang/vi.rs index d301e4e1737..7954cbe2100 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Mật khẩu hệ điều hành"), ("install_tip", "Do UAC, RustDesk sẽ không thể hoạt động đúng cách là bên từ xa trong vài trường hợp. Để tránh UAC, hãy nhấn cái nút dưới đây để cài RustDesk vào hệ thống."), ("Click to upgrade", "Nhấn để nâng cấp"), - ("Click to download", "Nhấn để tải xuống"), - ("Click to update", "Nhấn để cập nhật"), ("Configure", "Cài đặt"), ("config_acc", "Để có thể điều khiển máy tính từ xa, bạn cần phải cung cấp quyền \"Trợ năng\" cho RustDesk"), ("config_screen", "Để có thể truy cập máy tính từ xa, bạn cần phải cung cấp quyền \"Ghi Màn Hình\" cho RustDesk."), From 8899b907255291873b2d8a090b37f46d15efcf24 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 31 Jul 2025 00:27:55 +0800 Subject: [PATCH 460/506] fix: build (#12483) Signed-off-by: fufesou --- flutter/lib/desktop/pages/desktop_home_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index b975e9c6421..23769115919 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -434,7 +434,7 @@ class _DesktopHomePageState extends State !isCardClosed && bind.mainUriPrefixSync().contains('rustdesk')) { final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled(); - String btnText = isToUpdate ? 'Update' : "Download'; + String btnText = isToUpdate ? 'Update' : 'Download'; GestureTapCallback onPressed = () async { final Uri url = Uri.parse('https://rustdesk.com/download'); await launchUrl(url); From 6ec217263da6bc8173eca3339b41d6df8ac6a1ad Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:58:00 +0800 Subject: [PATCH 461/506] fix: nokhwa, win, infinite loop (#12489) Signed-off-by: fufesou --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c00b0ade108..7487b5c5207 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4369,7 +4369,7 @@ dependencies = [ [[package]] name = "nokhwa" version = "0.10.7" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#f32e7d68be61db9b1e99016b24edb14543d0383b" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" dependencies = [ "flume", "image 0.25.1", @@ -4384,7 +4384,7 @@ dependencies = [ [[package]] name = "nokhwa-bindings-linux" version = "0.1.1" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#f32e7d68be61db9b1e99016b24edb14543d0383b" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" dependencies = [ "nokhwa-core", "v4l", @@ -4393,7 +4393,7 @@ dependencies = [ [[package]] name = "nokhwa-bindings-macos" version = "0.2.2" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#f32e7d68be61db9b1e99016b24edb14543d0383b" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" dependencies = [ "block", "cocoa-foundation", @@ -4409,7 +4409,7 @@ dependencies = [ [[package]] name = "nokhwa-bindings-windows" version = "0.4.2" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#f32e7d68be61db9b1e99016b24edb14543d0383b" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" dependencies = [ "dlopen", "lazy_static", @@ -4421,7 +4421,7 @@ dependencies = [ [[package]] name = "nokhwa-core" version = "0.1.5" -source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#f32e7d68be61db9b1e99016b24edb14543d0383b" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" dependencies = [ "bytes", "image 0.25.1", From f32591c3d1dc7fc026b761225cbe38b6723065a5 Mon Sep 17 00:00:00 2001 From: Mahdi Rahimi <31624047+mahdirahimi1999@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:48:49 +0330 Subject: [PATCH 462/506] Update Arabic translation in ar.rs (#12451) --- src/lang/ar.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/ar.rs b/src/lang/ar.rs index edc9d7e98ac..5ef5e1d2cb3 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -700,13 +700,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "تمكين الطرفية"), ("New tab", "تبويب جديد"), ("Keep terminal sessions on disconnect", "الاحتفاظ بجلسات الطرفية عند قطع الاتصال"), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), - ("elevation_username_tip", ""), + ("Terminal (Run as administrator)", "الطرفية (تشغيل كمسؤول)"), + ("terminal-admin-login-tip", "لتشغيل الطرفية كمسؤول، يرجى إدخال اسم المستخدم وكلمة المرور للمسؤول."), + ("Failed to get user token.", "فشل في الحصول على رمز المستخدم."), + ("Incorrect username or password.", "اسم المستخدم أو كلمة المرور غير صحيحة."), + ("The user is not an administrator.", "المستخدم ليس لديه صلاحيات المسؤول."), + ("Failed to check if the user is an administrator.", "فشل التحقق مما إذا كان المستخدم لديه صلاحيات المسؤول."), + ("Supported only in the installed version.", "مدعوم فقط في النسخة المُثبتة."), + ("elevation_username_tip", "يرجى إدخال اسم مستخدم بصلاحيات المسؤول للمتابعة."), ].iter().cloned().collect(); } From 4e7680e32234b0a545500d62f8e3d68ccafa00ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Sat, 2 Aug 2025 13:05:19 +0900 Subject: [PATCH 463/506] Update ko.rs (#12480) * Update ko.rs * Update ko.rs --------- Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- src/lang/ko.rs | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index ef918844e7b..2e4cb67a4f0 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -5,7 +5,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Your Desktop", "내 데스크탑"), ("desk_tip", "이 ID와 비밀번호로 데스크톱에 액세스할 수 있습니다."), ("Password", "비밀번호"), - ("Ready", "준비"), + ("Ready", "준비 완료"), ("Established", "연결됨"), ("connecting_status", "RustDesk 네트워크에 연결 중..."), ("Enable service", "서비스 활성화"), @@ -22,7 +22,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("TCP tunneling", "TCP 터널링"), ("Remove", "삭제"), ("Refresh random password", "임의의 비밀번호 새로 고침"), - ("Set your own password", "나만의 비밀번호 설정"), + ("Set your own password", "자신만의 비밀번호 설정"), ("Enable keyboard/mouse", "키보드/마우스 사용함"), ("Enable clipboard", "클립보드 사용함"), ("Enable file transfer", "파일 전송 사용함"), @@ -77,7 +77,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Error", "오류"), ("Reset by the peer", "피어에 의해 초기화"), ("Connecting...", "연결 중..."), - ("Connection in progress. Please wait.", "연결이 진행 중입니다. 잠시만 기다려 주세요."), + ("Connection in progress. Please wait.", "연결이 진행 중입니다. 기다려 주세요."), ("Please try 1 minute later", "1분 후에 다시 시도하세요"), ("Login Error", "로그인 오류"), ("Successful", "성공"), @@ -158,9 +158,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Create desktop icon", "바탕 화면 아이콘 만들기"), ("agreement_tip", "설치를 시작하면 라이선스 계약을 수락하는 것입니다."), ("Accept and Install", "수락하고 설치"), - ("End-user license agreement", "최종 사용자 라이선스 약관 동의"), + ("End-user license agreement", "최종 사용자 라이선스 계약"), ("Generating ...", "생성 중 ..."), - ("Your installation is lower version.", "설치 버전이 하위 버전입니다."), + ("Your installation is lower version.", "설치된 버전이 낮습니다."), ("not_close_tcp_tip", "터널을 사용하는 동안에는 이 창을 닫지 마세요"), ("Listening ...", "청취 중 ..."), ("Remote Host", "원격 호스트"), @@ -291,7 +291,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Help", "도움말"), ("Failed", "실패"), ("Succeeded", "성공"), - ("Someone turns on privacy mode, exit", "누군가 개인정보 보호 모드를 켜고 종료합니다"), + ("Someone turns on privacy mode, exit", "누군가가 개인정보 보호 모드를 켭니다, 종료합니다"), ("Unsupported", "지원되지 않음"), ("Peer denied", "연결 거부됨"), ("Please install plugins", "플러그인을 설치해주세요"), @@ -359,8 +359,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin Toolbar", "도구 모음 고정 해제"), ("Recording", "녹화"), ("Directory", "디렉터리"), - ("Automatically record incoming sessions", "들어오는 세션 자동 녹화"), - ("Automatically record outgoing sessions", "나가는 세션 자동 녹화"), + ("Automatically record incoming sessions", "수신 세션 자동 녹화"), + ("Automatically record outgoing sessions", "발신 세션 자동 녹화"), ("Change", "변경"), ("Start session recording", "세션 녹화 시작"), ("Stop session recording", "세션 녹화 중지"), @@ -455,10 +455,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Minimize", "최소화"), ("Maximize", "최대화"), ("Your Device", "내 장치"), - ("empty_recent_tip", "최근 세션이 없습니다. 새 세션을 시작해보세요"), - ("empty_favorite_tip", "장치 즐겨찾기가 없습니다. 새 즐겨찾기를 추가해보세요"), - ("empty_lan_tip", "제어되는 장치가 발견되지 않았습니다."), - ("empty_address_book_tip", "현재 주소록에 제어되는 클라이언트가 없습니다"), + ("empty_recent_tip", "어머나, 최근 세션이 없네요!\n새로운 것을 계획할 시간입니다."), + ("empty_favorite_tip", "아직 즐겨찾는 피어가 없나요?\n연결하고 싶은 피어를 찾아 즐겨찾기에 추가해 보세요!"), + ("empty_lan_tip", "오 아니요, 아직 피어를 발견하지 못한 것 같습니다."), + ("empty_address_book_tip", "오, 이게 무슨 일인지 주소록에 현재 나열된 피어가 없는 것 같습니다."), ("Empty Username", "사용자 이름이 비어있습니다"), ("Empty Password", "비밀번호가 비어있습니다"), ("Me", "나"), @@ -498,8 +498,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept and Elevate", "수락 및 권한 상승"), ("accept_and_elevate_btn_tooltip", "연결을 수락하고 UAC 권한을 높입니다."), ("clipboard_wait_response_timeout_tip", "복사 응답을 기다리는 동안 시간이 초과되었습니다."), - ("Incoming connection", "들어오는 연결"), - ("Outgoing connection", "나가는 연결"), + ("Incoming connection", "수신 연결"), + ("Outgoing connection", "발신 연결"), ("Exit", "종료"), ("Open", "열기"), ("logout_tip", "로그아웃하시겠습니까?"), @@ -537,13 +537,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("I Agree", "동의"), ("Decline", "거절"), ("Timeout in minutes", "시간 초과 (분)"), - ("auto_disconnect_option_tip", "사용자가 비활성 상태일 때 들어오는 세션 자동 종료"), + ("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", "들어오는 세션 동안 배경화면 제거"), + ("Remove wallpaper during incoming sessions", "수신 세션 동안 배경화면 제거"), ("Test", "테스트"), ("display_is_plugged_out_msg", "디스플레이가 분리되어 있으면 첫 번째 디스플레이로 전환합니다."), ("No displays", "디스플레이 없음"), @@ -570,7 +570,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Swap control-command key", "Control 및 Command 키 교체"), ("swap-left-right-mouse", "마우스 왼쪽 버튼과 오른쪽 버튼 교체"), ("2FA code", "이중 인증 코드"), - ("More", "더 보기"), + ("More", "더 많은"), ("enable-2fa-title", "이중 인증 사용함"), ("enable-2fa-desc", "지금 인증앱을 설정해 주세요. 휴대폰이나 데스크톱에서 Authy, Microsoft 또는 Google 인증기와 같은 인증기 앱을 사용할 수 있습니다.\n\n앱으로 QR 코드를 스캔하고 앱에 표시된 코드를 입력하면 이중 인증이 가능합니다."), ("wrong-2fa-code", "코드를 확인할 수 없습니다. 코드와 현지 시간 설정이 올바른지 확인합니다"), @@ -661,9 +661,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-not-installed-tip", "{} 프린터가 설치되지 않았습니다."), ("printer-{}-ready-tip", "{} 프린터가 설치되어 사용할 준비가 되었습니다."), ("Install {} Printer", "{} 프린터 설치"), - ("Outgoing Print Jobs", "나가는 인쇄 작업"), - ("Incoming Print Jobs", "들어오는 인쇄 작업"), - ("Incoming Print Job", "들어오는 인쇄 작업"), + ("Outgoing Print Jobs", "발신 인쇄 작업"), + ("Incoming Print Jobs", "수신 인쇄 작업"), + ("Incoming Print Job", "수신 인쇄 작업"), ("use-the-default-printer-tip", "기본 프린터 사용"), ("use-the-selected-printer-tip", "선택한 프린터 사용"), ("auto-print-tip", "선택한 프린터를 사용하여 자동으로 인쇄합니다."), From 1f2f5a41d447391cf37bffe9dfcacfdd0b7fdafc Mon Sep 17 00:00:00 2001 From: tschettervictor <85497460+tschettervictor@users.noreply.github.com> Date: Sun, 3 Aug 2025 02:00:52 -0600 Subject: [PATCH 464/506] typo: openbad > openbsd (#12484) --- 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 fca6403f657..37b541cbe5a 100644 --- a/src/platform/gtk_sudo.rs +++ b/src/platform/gtk_sudo.rs @@ -465,7 +465,7 @@ fn ui_parent( 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 bsd = os == "freebsd" || os == "dragonfly" || os == "netbsd" || os == "openbsd"; let mut params = vec!["sudo".to_string()]; if su_user.is_some() { params.push("-S".to_string()); From 6533a1b98db49d5028ada971b52d4696aaf9c96e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H3X=C3=90=CE=9B=CE=9EM=D1=B2=D0=98?= <42803553+H3XDaemon@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:48:17 +0800 Subject: [PATCH 465/506] i18n(tw): Fix translations and address inconsistencies (#12490) --- 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 7e75af13fda..4957df477a7 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -413,7 +413,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", "傳送管理員的使用者名稱和密碼"), ("still_click_uac_tip", "依然需要遠端使用者在執行 RustDesk 時於 UAC 視窗點選「是」。"), @@ -493,7 +493,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Options", "選項"), ("resolution_original_tip", "原始解析度"), ("resolution_fit_local_tip", "調整成本機解析度"), - ("resolution_custom_tip", "自動解析度"), + ("resolution_custom_tip", "自訂解析度"), ("Collapse toolbar", "收回工具列"), ("Accept and Elevate", "接受並提升權限"), ("accept_and_elevate_btn_tooltip", "接受連線並提升 UAC 權限。"), @@ -521,7 +521,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select", "選擇"), ("Toggle Tags", "切換標籤"), ("pull_ab_failed_tip", "通訊錄更新失敗"), - ("push_ab_failed_tip", "成功同步通訊錄至伺服器"), + ("push_ab_failed_tip", "同步通訊錄至伺服器失敗"), ("synced_peer_readded_tip", "最近工作階段中存在的裝置將會被重新同步到通訊錄。"), ("Change Color", "更改顏色"), ("Primary Color", "基本色"), @@ -682,9 +682,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Downloading {}", "正在下載 {} 並安裝新版本。"), ("{} Update", "{} 更新"), ("{}-to-update-tip", "即將關閉 {} 並安裝新版本。"), - ("download-new-version-failed-tip", "安裝方式偵測失敗,請點擊\"下載\"按鈕以從發布網址下載,並手動升級。"), + ("download-new-version-failed-tip", "下載失敗,您可以重試或點擊\"下載\"按鈕以從發布網址下載,並手動升級。"), ("Auto update", "自動更新"), - ("update-failed-check-msi-tip", "下載失敗,您可以重試或點擊\"下載\"按鈕以從發布網址下載,並手動升級。"), + ("update-failed-check-msi-tip", "安裝方式偵測失敗,請點擊\"下載\"按鈕以從發布網址下載,並手動升級。"), ("websocket_tip", "使用 WebSocket 時,只支援使用中繼連接。"), ("Use WebSocket", "使用 WebSocket"), ("Trackpad speed", "觸控板速度"), From 2ba215a6d7014d3235c1addc5eb21872c229d8da Mon Sep 17 00:00:00 2001 From: asereze Date: Tue, 5 Aug 2025 19:45:24 +0200 Subject: [PATCH 466/506] Update sc.rs (#12517) --- src/lang/sc.rs | 64 +++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 3649e4519b8..1f46695b6ba 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -672,41 +672,41 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "Sas impostatziones de sos permissos de s'ala controllada negant s'imprenta remota."), ("save-settings-tip", "Sarva sas impostatziones"), ("dont-show-again-tip", "Non mustres prus custu messàgiu"), - ("Take screenshot", ""), - ("Taking screenshot", ""), - ("screenshot-merged-screen-not-supported-tip", ""), - ("screenshot-action-tip", ""), - ("Save as", ""), - ("Copy to clipboard", ""), - ("Enable remote printer", ""), - ("Downloading {}", ""), - ("{} Update", ""), - ("{}-to-update-tip", ""), - ("download-new-version-failed-tip", ""), - ("Auto update", ""), - ("update-failed-check-msi-tip", ""), - ("websocket_tip", ""), - ("Use WebSocket", ""), - ("Trackpad speed", ""), - ("Default trackpad speed", ""), - ("Numeric one-time password", ""), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("Take screenshot", "Faghe un'ischermada"), + ("Taking screenshot", "Faghende un'ischermada"), + ("screenshot-merged-screen-not-supported-tip", "S'unione de sa catura de ischermadas de prus ischermos como no est suportada.\nCola a un'ischermu ebbia e torra a proare."), + ("screenshot-action-tip", "Seletziona comente sighire cun s'ischermada."), + ("Save as", "Sarva comente"), + ("Copy to clipboard", "Còpia in punta de billete"), + ("Enable remote printer", "Abìlita imprentadora remota"), + ("Downloading {}", "Iscarrighende {}"), + ("{} Update", "Atualiza {}"), + ("{}-to-update-tip", "{} s'at a serrare e a installare sa versione nova"), + ("download-new-version-failed-tip", "Iscarrigamentu fallidu.\nPodes torrare a proare o seletzionare 'Iscàrriga' pro iscarrigare e atualizare a manera manuale."), + ("Auto update", "Atualizatzione automàtica"), + ("update-failed-check-msi-tip", "Controllu de sa manera de installatzione fallidu.\nSeletziona 'Iscàrriga' pro iscarrigare su programma e l'atualizare a manera manuale."), + ("websocket_tip", "Cando impreas WebSocket, sunt suportadas petzi sas connessiones de tràmuda relay"), + ("Use WebSocket", "Imprea WebSocket"), + ("Trackpad speed", "Velotzidade de su pannellu tàtile"), + ("Default trackpad speed", "Velotzidade predefinida de su pannellu tàtile"), + ("Numeric one-time password", "Crae numèrica monoimpreu"), + ("Enable IPv6 P2P connection", "Abìlita connessione P2P IPv6"), + ("Enable UDP hole punching", "Abìlita s'istampadura UDP"), ("View camera", "Mustra sa càmera"), ("Enable camera", "Abìlita sa càmera"), ("No cameras", "Peruna càmera"), ("view_camera_unsupported_tip", "Su dispositivu remotu non suportat sa visualizatzione de sa càmera"), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), - ("elevation_username_tip", ""), + ("Terminal", "Terminale"), + ("Enable terminal", "Abìlita su terminale"), + ("New tab", "Ischeda noa"), + ("Keep terminal sessions on disconnect", "Cando ti disconnetes mantene aberta sa sessione de terminale"), + ("Terminal (Run as administrator)", "Terminale (imprea comente amministradore)"), + ("terminal-admin-login-tip", "Inserta su nùmene utente e sa crae de intrada de s'amministradore de s'ala controllada."), + ("Failed to get user token.", "Otenimentu de su getone de utente fallidu."), + ("Incorrect username or password.", "Nùmene utente o crae de intrada isballiados."), + ("The user is not an administrator.", "S'utente no est un'amministradore."), + ("Failed to check if the user is an administrator.", "Non faghet a verificare si s'utente est un'amministradore."), + ("Supported only in the installed version.", "Suportadu petzi in sa versione installada."), + ("elevation_username_tip", "Inserta Nùmene utente o domìniu de fonte\\nùmene Utente"), ].iter().cloned().collect(); } From 725a47268ededfcaea69d457db0d4a8796b71659 Mon Sep 17 00:00:00 2001 From: Andrzej Rudnik Date: Wed, 6 Aug 2025 17:16:35 +0200 Subject: [PATCH 467/506] Updated Polish translation (#12521) * Update pl.rs * Update pl.rs --- src/lang/pl.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 97befe1eeae..d41f57d5d22 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -696,17 +696,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable camera", "Włącz kamerę"), ("No cameras", "Brak kamer"), ("view_camera_unsupported_tip", "Zdalne urządzenie nie obsługuje podglądu kamery."), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), - ("elevation_username_tip", ""), + ("Terminal", "Rerminal"), + ("Enable terminal", "Włącz terminal"), + ("New tab", "Nowa zakładka"), + ("Keep terminal sessions on disconnect", "Utrzymaj sesję terminala przy rozłączeniu"), + ("Terminal (Run as administrator)", "Terminal (uruchom jako administrator)"), + ("terminal-admin-login-tip", "Proszę wprowadzić użytkownika i hasło administratora kontrolowanego urządzenia."), + ("Failed to get user token.", "Błąd pobierania tokenu użytkownika."), + ("Incorrect username or password.", "Nieprawidłowy użytkownik lub hasło."), + ("The user is not an administrator.", "Użytkownik nie posiada praw administratora."), + ("Failed to check if the user is an administrator.", "Błąd sprawdzania, czy użytkownik jest administratorem."), + ("Supported only in the installed version.", "Wspierane tylko dla zainstalowanej aplikacji."), + ("elevation_username_tip", "Podaj nazwę użytkownika lub domena\\użytkownik"), ].iter().cloned().collect(); } From 77be752ff175b0059af02ccabbc3cbd5996610b1 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 7 Aug 2025 13:29:21 +0800 Subject: [PATCH 468/506] sciter hide cm (#12570) Signed-off-by: 21pages --- src/ui.rs | 16 ++++++++++++++-- src/ui/cm.rs | 9 +++++++++ src/ui/cm.tis | 19 +++++++++++++------ 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 6bef48ba26a..6bf7c68dadc 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -118,6 +118,11 @@ pub fn start(args: &mut [String]) { Box::new(cm::SciterConnectionManager::new()) }); page = "cm.html"; + *cm::HIDE_CM.lock().unwrap() = crate::ipc::get_config("hide_cm") + .ok() + .flatten() + .unwrap_or_default() + == "true"; } else if (args[0] == "--connect" || args[0] == "--file-transfer" || args[0] == "--port-forward" @@ -178,6 +183,13 @@ pub fn start(args: &mut [String]) { .unwrap_or("".to_owned()), page )); + let hide_cm = *cm::HIDE_CM.lock().unwrap(); + if !args.is_empty() && args[0] == "--cm" && hide_cm { + // run_app calls expand(show) + run_loop, we use collapse(hide) + run_loop instead to create a hidden window + frame.collapse(true); + frame.run_loop(); + return; + } frame.run_app(); } @@ -633,9 +645,9 @@ impl UI { pub fn verify2fa(&self, code: String) -> bool { verify2fa(code) } - + fn verify_login(&self, raw: String, id: String) -> bool { - crate::verify_login(&raw, &id) + crate::verify_login(&raw, &id) } fn generate_2fa_img_src(&self, data: String) -> String { diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 57e6b37dd16..92cd2e2f22b 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -7,6 +7,10 @@ use sciter::{make_args, Element, Value, HELEMENT}; use std::sync::Mutex; use std::{ops::Deref, sync::Arc}; +lazy_static::lazy_static! { + pub static ref HIDE_CM: Arc> = Arc::new(Mutex::new(false)); +} + #[derive(Clone, Default)] pub struct SciterHandler { pub element: Arc>>, @@ -151,6 +155,10 @@ impl SciterConnectionManager { fn get_option(&self, key: String) -> String { crate::ui_interface::get_option(key) } + + fn hide_cm(&self) -> bool { + *crate::ui::cm::HIDE_CM.lock().unwrap() + } } impl sciter::EventHandler for SciterConnectionManager { @@ -172,5 +180,6 @@ impl sciter::EventHandler for SciterConnectionManager { fn can_elevate(); fn elevate_portable(i32); fn get_option(String); + fn hide_cm(); } } diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 479e26f9283..0b0165b7374 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -6,6 +6,13 @@ var show_chat = false; var show_elevation = true; var svg_elevate = ; +var hide_cm = undefined; +function setWindowState(state) { + if (hide_cm == undefined) hide_cm = handler.hide_cm(); + if (hide_cm) return; + view.windowState = state; +} + class Body: Reactor.Component { this var cur = 0; @@ -163,7 +170,7 @@ class Body: Reactor.Component body.update(); handler.authorize(cid); self.timer(30ms, function() { - view.windowState = View.WINDOW_MINIMIZED; + setWindowState(View.WINDOW_MINIMIZED); }); }); } @@ -177,7 +184,7 @@ class Body: Reactor.Component handler.elevate_portable(cid); handler.authorize(cid); self.timer(30ms, function() { - view.windowState = View.WINDOW_MINIMIZED; + setWindowState(View.WINDOW_MINIMIZED); }); }); } @@ -189,7 +196,7 @@ class Body: Reactor.Component body.update(); handler.elevate_portable(cid); self.timer(30ms, function() { - view.windowState = View.WINDOW_MINIMIZED; + setWindowState(View.WINDOW_MINIMIZED); }); }); } @@ -350,7 +357,7 @@ function bring_to_top(idx=-1) { if (is_linux) { view.focus = self; } else { - view.windowState = View.WINDOW_SHOWN; + setWindowState(View.WINDOW_SHOWN); } if (idx >= 0) body.cur = idx; } else { @@ -396,7 +403,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin self.timer(1ms, adjustHeader); if (authorized) { self.timer(3s, function() { - view.windowState = View.WINDOW_MINIMIZED; + setWindowState(View.WINDOW_MINIMIZED); }); } } @@ -509,7 +516,7 @@ var tm0 = getTime(); function self.closing() { if (connections.length == 0 && getTime() - tm0 > 30000) return true; - view.windowState = View.WINDOW_HIDDEN; + setWindowState(View.WINDOW_HIDDEN); return false; } From b37b271fce7f283678cbedd813d545eca665ae98 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 1 Aug 2025 09:41:05 +0800 Subject: [PATCH 469/506] add team to osx --- flutter/macos/Runner.xcodeproj/project.pbxproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index f38badcbb36..c41bfa11744 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -433,7 +433,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = HZF9JMC8YN; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -579,7 +579,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = HZF9JMC8YN; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -609,7 +609,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = HZF9JMC8YN; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( From 9538eba64e06bdd326f7c0dc821fc16a3dc447a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Thu, 7 Aug 2025 21:15:47 +0900 Subject: [PATCH 470/506] Update ko.rs (#12523) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because it is button-shaped, even a short phrase such as “upgrade” can convey meaning in Korean. --- 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 2e4cb67a4f0..b8316a03975 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -146,7 +146,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set Password", "비밀번호 설정"), ("OS Password", "OS 비밀번호"), ("install_tip", "UAC로 인해 경우에 따라 RustDesk가 원격 쪽에서 제대로 작동하지 않을 수 있습니다. UAC를 피하려면 아래 버튼을 클릭하여 시스템에 RustDesk를 설치하세요."), - ("Click to upgrade", "업그레이드하려면 클릭"), + ("Click to upgrade", "업그레이드"), ("Configure", "구성"), ("config_acc", "데스크톱을 원격으로 제어하려면 RustDesk에 \"접근성\" 권한을 부여해야 합니다."), ("config_screen", "데스크톱에 원격으로 액세스하려면 RustDesk에 \"화면 녹화\" 권한을 부여해야 합니다."), From e7f672899bac27a3855984df8b24379ef5f03713 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Thu, 7 Aug 2025 14:15:59 +0200 Subject: [PATCH 471/506] Update nl.rs (#12525) --- 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 0b352a6241e..6cc320f24da 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -700,13 +700,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Terminal inschakelen"), ("New tab", "Nieuw tabblad"), ("Keep terminal sessions on disconnect", "Terminalsessies bij verbreking van de verbinding behouden"), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), - ("elevation_username_tip", ""), + ("Terminal (Run as administrator)", "Terminal (Als administrator uitvoeren)"), + ("terminal-admin-login-tip", "Voer de gebruikersnaam en het wachtwoord in van de beheerder van het gecontroleerde apparaat."), + ("Failed to get user token.", "Kan geen gebruikerstoken krijgen."), + ("Incorrect username or password.", "Foutieve gebruikersnaam of wachtwoord."), + ("The user is not an administrator.", "De gebruiker is geen beheerder."), + ("Failed to check if the user is an administrator.", "Fout bij het controleren of de gebruiker een beheerder is."), + ("Supported only in the installed version.", "Alleen ondersteund in de geïnstalleerde versie."), + ("elevation_username_tip", "Voer je gebruikersnaam of domeinnaam in"), ].iter().cloned().collect(); } From e85989e9d9f51ca00a08833e6edb6c537e41fc2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Thu, 7 Aug 2025 15:16:14 +0300 Subject: [PATCH 472/506] Fix Turkish localization (#12555) --- src/lang/tr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/tr.rs b/src/lang/tr.rs index cff87d8555e..ff3d3628595 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -135,7 +135,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Refresh", "Yenile"), ("ID does not exist", "ID bulunamadı"), ("Failed to connect to rendezvous server", "ID oluşturma sunucusuna bağlanılamadı"), - ("Please try later", "Dağa sonra tekrar deneyiniz"), + ("Please try later", "Daha sonra tekrar deneyiniz"), ("Remote desktop is offline", "Uzak masaüstü kapalı"), ("Key mismatch", "Anahtar uyumlu değil"), ("Timeout", "Zaman aşımı"), From 39b91911cbca39fdf702f2c48314cccc9e8d4a73 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 7 Aug 2025 23:31:31 +0800 Subject: [PATCH 473/506] fix: update macos (#12578) * fix: update macos 1. Use `ditto` instead of `cp -r`. 2. Add prompt for extracting dmg. Signed-off-by: fufesou * fix: error to err Signed-off-by: fufesou * Refact: Remove "Extracting" Signed-off-by: fufesou --------- Signed-off-by: fufesou Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- .../lib/desktop/widgets/update_progress.dart | 80 +++++++++++++------ src/flutter_ffi.rs | 20 ++++- src/platform/macos.rs | 24 +++++- src/platform/privileges_scripts/update.scpt | 2 +- 4 files changed, 96 insertions(+), 30 deletions(-) diff --git a/flutter/lib/desktop/widgets/update_progress.dart b/flutter/lib/desktop/widgets/update_progress.dart index ac425fa2bda..fd948b7907e 100644 --- a/flutter/lib/desktop/widgets/update_progress.dart +++ b/flutter/lib/desktop/widgets/update_progress.dart @@ -7,7 +7,10 @@ import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher.dart'; +final _isExtracting = false.obs; + void handleUpdate(String releasePageUrl) { + _isExtracting.value = false; String downloadUrl = releasePageUrl.replaceAll('tag', 'download'); String version = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1); final String downloadFile = @@ -25,13 +28,14 @@ void handleUpdate(String releasePageUrl) { gFFI.dialogManager.dismissAll(); gFFI.dialogManager.show((setState, close, context) { return CustomAlertDialog( - title: Text(translate('Downloading {$appName}')), + title: Obx(() => Text(translate( + _isExtracting.isTrue ? 'Installing ...' : 'Downloading {$appName}'))), content: UpdateProgress(releasePageUrl, downloadUrl, downloadId, onCanceled) .marginSymmetric(horizontal: 8) .paddingOnly(top: 12), actions: [ - dialogButton(translate('Cancel'), onPressed: () async { + if (_isExtracting.isFalse) dialogButton(translate('Cancel'), onPressed: () async { onCanceled.value(); await bind.mainSetCommon( key: 'cancel-downloader', value: downloadId.value); @@ -71,6 +75,7 @@ class UpdateProgressState extends State { int _downloadedSize = 0; int _getDataFailedCount = 0; final String _eventKeyDownloadNewVersion = 'download-new-version'; + final String _eventKeyExtractUpdateDmg = 'extract-update-dmg'; @override void initState() { @@ -82,6 +87,11 @@ class UpdateProgressState extends State { _eventKeyDownloadNewVersion, handleDownloadNewVersion, replace: true); bind.mainSetCommon(key: 'download-new-version', value: widget.downloadUrl); + if (isMacOS) { + platformFFI.registerEventHandler(_eventKeyExtractUpdateDmg, + _eventKeyExtractUpdateDmg, handleExtractUpdateDmg, + replace: true); + } } @override @@ -89,6 +99,10 @@ class UpdateProgressState extends State { cancelQueryTimer(); platformFFI.unregisterEventHandler( _eventKeyDownloadNewVersion, _eventKeyDownloadNewVersion); + if (isMacOS) { + platformFFI.unregisterEventHandler( + _eventKeyExtractUpdateDmg, _eventKeyExtractUpdateDmg); + } super.dispose(); } @@ -113,10 +127,13 @@ class UpdateProgressState extends State { } } - void _onError(String error) { + // `isExtractDmg` is true when handling extract-update-dmg event. + // It's a rare case that the dmg file is corrupted and cannot be extracted. + void _onError(String error, {bool isExtractDmg = false}) { cancelQueryTimer(); - debugPrint('Download new version error: $error'); + debugPrint( + '${isExtractDmg ? "Extract" : "Download"} new version error: $error'); final msgBoxType = 'custom-nocancel-nook-hasclose'; final msgBoxTitle = 'Error'; final msgBoxText = 'download-new-version-failed-tip'; @@ -138,7 +155,7 @@ class UpdateProgressState extends State { final List buttons = [ dialogButton('Download', onPressed: jumplink), - dialogButton('Retry', onPressed: retry), + if (!isExtractDmg) dialogButton('Retry', onPressed: retry), dialogButton('Close', onPressed: close), ]; dialogManager.dismissAll(); @@ -194,19 +211,13 @@ class UpdateProgressState extends State { _onError('The download file size is 0.'); } else { setState(() {}); - msgBox( - gFFI.sessionId, - 'custom-nocancel', - '{$appName} Update', - '{$appName}-to-update-tip', - '', - gFFI.dialogManager, - onSubmit: () { - debugPrint('Downloaded, update to new version now'); - bind.mainSetCommon(key: 'update-me', value: widget.downloadUrl); - }, - submitTimeout: 5, - ); + if (isMacOS) { + bind.mainSetCommon( + key: 'extract-update-dmg', value: widget.downloadUrl); + _isExtracting.value = true; + } else { + updateMsgBox(); + } } } else { setState(() {}); @@ -214,17 +225,38 @@ class UpdateProgressState extends State { } } - @override - Widget build(BuildContext context) { - return onDownloading(context); + void updateMsgBox() { + msgBox( + gFFI.sessionId, + 'custom-nocancel', + '{$appName} Update', + '{$appName}-to-update-tip', + '', + gFFI.dialogManager, + onSubmit: () { + debugPrint('Downloaded, update to new version now'); + bind.mainSetCommon(key: 'update-me', value: widget.downloadUrl); + }, + submitTimeout: 5, + ); } - Widget onDownloading(BuildContext context) { - final value = _totalSize == null + Future handleExtractUpdateDmg(Map evt) async { + _isExtracting.value = false; + if (evt.containsKey('err') && (evt['err'] as String).isNotEmpty) { + _onError(evt['err'] as String, isExtractDmg: true); + } else { + updateMsgBox(); + } + } + + @override + Widget build(BuildContext context) { + getValue() => _totalSize == null ? 0.0 : (_totalSize == 0 ? 1.0 : _downloadedSize / _totalSize!); return LinearProgressIndicator( - value: value, + value: _isExtracting.isTrue ? null : getValue(), minHeight: 20, borderRadius: BorderRadius.circular(5), backgroundColor: Colors.grey[300], diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 58afae52814..3e947609f85 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -629,7 +629,10 @@ pub fn session_open_terminal(session_id: SessionID, terminal_id: i32, rows: u32, if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.open_terminal(terminal_id, rows, cols); } else { - log::error!("[flutter_ffi] Session not found for session_id: {}", session_id); + log::error!( + "[flutter_ffi] Session not found for session_id: {}", + session_id + ); } } @@ -2651,6 +2654,21 @@ pub fn main_set_common(_key: String, _value: String) { fs::remove_file(f).ok(); } } + } else if _key == "extract-update-dmg" { + #[cfg(target_os = "macos")] + { + if let Some(new_version_file) = get_download_file_from_url(&_value) { + if let Some(f) = new_version_file.to_str() { + crate::platform::macos::extract_update_dmg(f); + } else { + // unreachable!() + log::error!("Failed to get the new version file path"); + } + } else { + // unreachable!() + log::error!("Failed to get the new version file from url: {}", _value); + } + } } } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index ac5e47f6755..c525af7492c 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -28,6 +28,7 @@ use objc::rc::autoreleasepool; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; use std::{ + collections::HashMap, os::unix::process::CommandExt, path::{Path, PathBuf}, process::{Command, Stdio}, @@ -743,7 +744,7 @@ pub fn update_me() -> ResultType<()> { let update_body = format!( r#" do shell script " -pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDesk.app && cp -R '{}' /Applications/ && chown -R {}:staff /Applications/RustDesk.app +pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDesk.app && ditto '{}' /Applications/RustDesk.app && chown -R {}:staff /Applications/RustDesk.app && xattr -r -d com.apple.quarantine /Applications/RustDesk.app " with prompt "RustDesk wants to update itself" with administrator privileges "#, std::process::id(), @@ -775,11 +776,26 @@ pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDes } pub fn update_to(file: &str) -> ResultType<()> { - extract_dmg(file, UPDATE_TEMP_DIR)?; update_extracted(UPDATE_TEMP_DIR)?; Ok(()) } +pub fn extract_update_dmg(file: &str) { + let mut evt: HashMap<&str, String> = + HashMap::from([("name", "extract-update-dmg".to_string())]); + match extract_dmg(file, UPDATE_TEMP_DIR) { + Ok(_) => { + log::info!("Extracted dmg file to {}", UPDATE_TEMP_DIR); + } + Err(e) => { + evt.insert("err", e.to_string()); + log::error!("Failed to extract dmg file {}: {}", file, e); + } + } + let evt = serde_json::ser::to_string(&evt).unwrap_or("".to_owned()); + crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, evt); +} + fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { let mount_point = "/Volumes/RustDeskUpdate"; let target_path = Path::new(target_dir); @@ -807,8 +823,8 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { let src_path = format!("{}/{}", mount_point, app_name); let dest_path = format!("{}/{}", target_dir, app_name); - let copy_status = Command::new("cp") - .args(&["-R", &src_path, &dest_path]) + let copy_status = Command::new("ditto") + .args(&[&src_path, &dest_path]) .status()?; if !copy_status.success() { diff --git a/src/platform/privileges_scripts/update.scpt b/src/platform/privileges_scripts/update.scpt index f9faa4aae49..dffb70bd7d5 100644 --- a/src/platform/privileges_scripts/update.scpt +++ b/src/platform/privileges_scripts/update.scpt @@ -4,7 +4,7 @@ on run {daemon_file, agent_file, user, cur_pid, source_dir} set kill_others to "pgrep -x 'RustDesk' | grep -v " & cur_pid & " | xargs kill -9;" - set copy_files to "rm -rf /Applications/RustDesk.app && cp -r " & source_dir & " /Applications && chown -R " & quoted form of user & ":staff /Applications/RustDesk.app;" + set copy_files to "rm -rf /Applications/RustDesk.app && ditto " & source_dir & " /Applications/RustDesk.app && chown -R " & quoted form of user & ":staff /Applications/RustDesk.app && xattr -r -d com.apple.quarantine /Applications/RustDesk.app;" set sh1 to "echo " & quoted form of daemon_file & " > /Library/LaunchDaemons/com.carriez.RustDesk_service.plist && chown root:wheel /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;" From 6bc3b38b56edec42e809811a8e2c8716154da4eb Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:25:22 +0800 Subject: [PATCH 474/506] refact: macos, update, preparing for installation (#12581) Signed-off-by: fufesou --- flutter/lib/desktop/widgets/update_progress.dart | 5 +++-- 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/ge.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/sc.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/ta.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/vi.rs | 1 + 47 files changed, 49 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/update_progress.dart b/flutter/lib/desktop/widgets/update_progress.dart index fd948b7907e..93f661b7b7d 100644 --- a/flutter/lib/desktop/widgets/update_progress.dart +++ b/flutter/lib/desktop/widgets/update_progress.dart @@ -28,8 +28,9 @@ void handleUpdate(String releasePageUrl) { gFFI.dialogManager.dismissAll(); gFFI.dialogManager.show((setState, close, context) { return CustomAlertDialog( - title: Obx(() => Text(translate( - _isExtracting.isTrue ? 'Installing ...' : 'Downloading {$appName}'))), + title: Obx(() => Text(translate(_isExtracting.isTrue + ? 'Preparing for installation ...' + : 'Downloading {$appName}'))), content: UpdateProgress(releasePageUrl, downloadUrl, downloadId, onCanceled) .marginSymmetric(horizontal: 8) diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 5ef5e1d2cb3..28b1e74f7f3 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "فشل التحقق مما إذا كان المستخدم لديه صلاحيات المسؤول."), ("Supported only in the installed version.", "مدعوم فقط في النسخة المُثبتة."), ("elevation_username_tip", "يرجى إدخال اسم مستخدم بصلاحيات المسؤول للمتابعة."), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index a57802bf11d..61a4ed6c31c 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 9988ead28bf..5b71674d3eb 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index fc228bb8b3a..66067b261c9 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 18fba8d5483..ef28c34fc35 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "检查用户是否为管理员时出错。"), ("Supported only in the installed version.", "仅在以安装版本受支持。"), ("elevation_username_tip", "输入用户名或域名\\用户名"), + ("Preparing for installation ...", "准备安装..."), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 30aac8df667..dc4e0f214e7 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 7870767e428..b180c5856a5 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 05e145c2255..4a703f4ba83 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "Es konnte nicht geprüft werden, ob der Benutzer ein Administrator ist."), ("Supported only in the installed version.", "Wird nur in der installierten Version unterstützt."), ("elevation_username_tip", "Geben Sie Benutzername oder Domäne\\Benutzername ein"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 22418bb007f..56704fb392b 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 4ef2476f215..3447df9f442 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 35297ca78d4..471d7bd73db 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "No se ha podido comprobar si el usuario es un administrador."), ("Supported only in the installed version.", "Soportado solo en la versión instalada."), ("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 70cd9267b73..507b580c4f5 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 914a4eb62bc..769c3788f2a 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index a68951ff25b..44d40d1e790 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "بررسی وضعیت مدیر سیستم برای کاربر ناموفق بود."), ("Supported only in the installed version.", "فقط در نسخه نصب‌شده پشتیبانی می‌شود."), ("elevation_username_tip", "لطفاً نام کاربری مدیریتی را برای ارتقاء دسترسی وارد کنید."), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index b66d1c2c241..6b2b2816dfa 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "Échec de la vérification du statut d’administrateur de l’utilisateur."), ("Supported only in the installed version.", "Uniquement pris en charge dans la version installée."), ("elevation_username_tip", "Saisissez un nom d’utilisateur ou un domaine\\utilisateur"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index db2be1836a2..168752abc1c 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index e4299b00945..54d44f6c576 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index f3a900acee4..8339b16f2d0 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index daedab9c771..df0f716f6e3 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "Hiba merült fel annak ellenőrzése során, hogy a felhasználó rendszergazda-e."), ("Supported only in the installed version.", "Csak a telepített változatban támogatott."), ("elevation_username_tip", "Felhasználónév vagy tartománynév megadása\\felhasználónév"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 6718a719af7..ed179729e78 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index f52a1be2ba4..613c4ce16ae 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "Impossibile verificare se l'utente è un amministratore."), ("Supported only in the installed version.", "Supportato solo nella versione installata."), ("elevation_username_tip", "Inserisci Nome utente o dominio sorgente\\nome Utente"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 597f43c1ffc..eeedf0c6183 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index b8316a03975..a880bb86a7b 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "사용자가 관리자인지 확인하는 데 실패했습니다."), ("Supported only in the installed version.", "설치된 버전에서만 지원됩니다."), ("elevation_username_tip", "사용자 이름 또는 도메인\\사용자 이름 입력"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 467d3fcea1b..a48f7c946c7 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index a35ff066061..72df3e737b4 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 126ad075e3e..1b4beb30176 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index cec6ec3a183..6b0c4f29d0e 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 6cc320f24da..706bb341bc2 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "Fout bij het controleren of de gebruiker een beheerder is."), ("Supported only in the installed version.", "Alleen ondersteund in de geïnstalleerde versie."), ("elevation_username_tip", "Voer je gebruikersnaam of domeinnaam in"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index d41f57d5d22..e08d65f2872 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "Błąd sprawdzania, czy użytkownik jest administratorem."), ("Supported only in the installed version.", "Wspierane tylko dla zainstalowanej aplikacji."), ("elevation_username_tip", "Podaj nazwę użytkownika lub domena\\użytkownik"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index cbb9aa7c93a..bbfd265936b 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 7058fd7b1f1..1a41dc307b9 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index ce912cb35c5..93eb232da76 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index c200cf77481..eb0de735577 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "Невозможно проверить, является ли пользователь администратором."), ("Supported only in the installed version.", "Поддерживается только в установочной версии."), ("elevation_username_tip", "Введите пользователя или домен\\пользователя"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 1f46695b6ba..73a7161bdc9 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "Non faghet a verificare si s'utente est un'amministradore."), ("Supported only in the installed version.", "Suportadu petzi in sa versione installada."), ("elevation_username_tip", "Inserta Nùmene utente o domìniu de fonte\\nùmene Utente"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 580486e8519..c32168c70dd 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 2edcf26ce63..021b6dabef4 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index a054314d970..a8a1a061f91 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index a8bc4e0d350..f26db2360b1 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index f394b90a390..9e495ba0131 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index b925e40db1a..e642180d9f6 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 8b311303b0b..d1f7788354f 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index a0fd34042ef..671491695bb 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index ff3d3628595..28b649daab8 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 4957df477a7..1c9365c6b83 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "檢查使用者是否是系統管理員時失敗了"), ("Supported only in the installed version.", "僅支援於已安裝的版本"), ("elevation_username_tip", "輸入使用者名稱或網域\\使用者名稱"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 98afe623833..8ea7805aa3e 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 7954cbe2100..e6faa4f31ba 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -708,5 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", ""), ("Supported only in the installed version.", ""), ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } From 466d456760014619ae4f663dcb2e21bad3e2d2db Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 9 Aug 2025 10:25:21 +0800 Subject: [PATCH 475/506] fix https://github.com/rustdesk/rustdesk/issues/12587 --- res/rustdesk-link.desktop | 2 +- res/setup.nsi | 178 -------------------------------------- 2 files changed, 1 insertion(+), 179 deletions(-) delete mode 100644 res/setup.nsi diff --git a/res/rustdesk-link.desktop b/res/rustdesk-link.desktop index c6f4d3f2a79..c7a9bd5cb7a 100644 --- a/res/rustdesk-link.desktop +++ b/res/rustdesk-link.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Name=RustDeskURL Scheme Handler +Name=RustDesk NoDisplay=true MimeType=x-scheme-handler/rustdesk; TryExec=rustdesk diff --git a/res/setup.nsi b/res/setup.nsi deleted file mode 100644 index 21cee15c80f..00000000000 --- a/res/setup.nsi +++ /dev/null @@ -1,178 +0,0 @@ -Unicode true - -#################################################################### -# Includes - -!include nsDialogs.nsh -!include MUI2.nsh -!include x64.nsh -!include LogicLib.nsh - -#################################################################### -# File Info - -!define PRODUCT_NAME "RustDesk" -!define PRODUCT_DESCRIPTION "Installer for ${PRODUCT_NAME}" -!define COPYRIGHT "Copyright © 2021" -!define VERSION "1.1.6" - -VIProductVersion "${VERSION}.0" -VIAddVersionKey "ProductName" "${PRODUCT_NAME}" -VIAddVersionKey "ProductVersion" "${VERSION}" -VIAddVersionKey "FileDescription" "${PRODUCT_DESCRIPTION}" -VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" -VIAddVersionKey "FileVersion" "${VERSION}.0" - -#################################################################### -# Installer Attributes - -Name "${PRODUCT_NAME}" -Outfile "rustdesk-${VERSION}-setup.exe" -Caption "Setup - ${PRODUCT_NAME}" -BrandingText "${PRODUCT_NAME}" - -ShowInstDetails show -RequestExecutionLevel admin -SetOverwrite on - -InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}" - -#################################################################### -# Pages - -!define MUI_ICON "icon.ico" -!define MUI_ABORTWARNING -!define MUI_LANGDLL_ALLLANGUAGES -!define MUI_FINISHPAGE_SHOWREADME "" -!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED -!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create desktop shortcut" -!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut -!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_NAME}.exe" - -!insertmacro MUI_PAGE_DIRECTORY -!insertmacro MUI_PAGE_INSTFILES -!insertmacro MUI_PAGE_FINISH - -#################################################################### -# Language - -!insertmacro MUI_LANGUAGE "English" ; The first language is the default language -!insertmacro MUI_LANGUAGE "French" -!insertmacro MUI_LANGUAGE "German" -!insertmacro MUI_LANGUAGE "Spanish" -!insertmacro MUI_LANGUAGE "SpanishInternational" -!insertmacro MUI_LANGUAGE "SimpChinese" -!insertmacro MUI_LANGUAGE "TradChinese" -!insertmacro MUI_LANGUAGE "Japanese" -!insertmacro MUI_LANGUAGE "Korean" -!insertmacro MUI_LANGUAGE "Italian" -!insertmacro MUI_LANGUAGE "Dutch" -!insertmacro MUI_LANGUAGE "Danish" -!insertmacro MUI_LANGUAGE "Swedish" -!insertmacro MUI_LANGUAGE "Norwegian" -!insertmacro MUI_LANGUAGE "NorwegianNynorsk" -!insertmacro MUI_LANGUAGE "Finnish" -!insertmacro MUI_LANGUAGE "Greek" -!insertmacro MUI_LANGUAGE "Russian" -!insertmacro MUI_LANGUAGE "Portuguese" -!insertmacro MUI_LANGUAGE "PortugueseBR" -!insertmacro MUI_LANGUAGE "Polish" -!insertmacro MUI_LANGUAGE "Ukrainian" -!insertmacro MUI_LANGUAGE "Czech" -!insertmacro MUI_LANGUAGE "Slovak" -!insertmacro MUI_LANGUAGE "Croatian" -!insertmacro MUI_LANGUAGE "Bulgarian" -!insertmacro MUI_LANGUAGE "Hungarian" -!insertmacro MUI_LANGUAGE "Thai" -!insertmacro MUI_LANGUAGE "Romanian" -!insertmacro MUI_LANGUAGE "Latvian" -!insertmacro MUI_LANGUAGE "Macedonian" -!insertmacro MUI_LANGUAGE "Estonian" -!insertmacro MUI_LANGUAGE "Turkish" -!insertmacro MUI_LANGUAGE "Lithuanian" -!insertmacro MUI_LANGUAGE "Slovenian" -!insertmacro MUI_LANGUAGE "Serbian" -!insertmacro MUI_LANGUAGE "SerbianLatin" -!insertmacro MUI_LANGUAGE "Arabic" -!insertmacro MUI_LANGUAGE "Farsi" -!insertmacro MUI_LANGUAGE "Hebrew" -!insertmacro MUI_LANGUAGE "Indonesian" -!insertmacro MUI_LANGUAGE "Mongolian" -!insertmacro MUI_LANGUAGE "Luxembourgish" -!insertmacro MUI_LANGUAGE "Albanian" -!insertmacro MUI_LANGUAGE "Breton" -!insertmacro MUI_LANGUAGE "Belarusian" -!insertmacro MUI_LANGUAGE "Icelandic" -!insertmacro MUI_LANGUAGE "Malay" -!insertmacro MUI_LANGUAGE "Bosnian" -!insertmacro MUI_LANGUAGE "Kurdish" -!insertmacro MUI_LANGUAGE "Irish" -!insertmacro MUI_LANGUAGE "Uzbek" -!insertmacro MUI_LANGUAGE "Galician" -!insertmacro MUI_LANGUAGE "Afrikaans" -!insertmacro MUI_LANGUAGE "Catalan" -!insertmacro MUI_LANGUAGE "Esperanto" -!insertmacro MUI_LANGUAGE "Asturian" -!insertmacro MUI_LANGUAGE "Basque" -!insertmacro MUI_LANGUAGE "Pashto" -!insertmacro MUI_LANGUAGE "ScotsGaelic" -!insertmacro MUI_LANGUAGE "Georgian" -!insertmacro MUI_LANGUAGE "Vietnamese" -!insertmacro MUI_LANGUAGE "Welsh" -!insertmacro MUI_LANGUAGE "Armenian" -!insertmacro MUI_LANGUAGE "Corsican" -!insertmacro MUI_LANGUAGE "Tatar" -!insertmacro MUI_LANGUAGE "Hindi" - - -#################################################################### -# Sections - -Section "Install" - SetOutPath $INSTDIR - - # Regkeys - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayIcon" "$INSTDIR\${PRODUCT_NAME}.exe" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME} (x64)" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayVersion" "${VERSION}" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString" '"$INSTDIR\${PRODUCT_NAME}.exe" --uninstall' - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "InstallLocation" "$INSTDIR" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "Publisher" "Purslane Ltd." - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "HelpLink" "https://www.rustdesk.com/" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLInfoAbout" "https://www.rustdesk.com/" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLUpdateInfo" "https://www.rustdesk.com/" - - nsExec::Exec "taskkill /F /IM ${PRODUCT_NAME}.exe" - Sleep 500 ; Give time for process to be completely killed - File "${PRODUCT_NAME}.exe" - - SetShellVarContext all - CreateShortCut "$INSTDIR\Uninstall ${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--uninstall" "msiexec.exe" - CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" - CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" - CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall ${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--uninstall" "msiexec.exe" - CreateShortCut "$SMSTARTUP\${PRODUCT_NAME} Tray.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--tray" - - nsExec::Exec 'sc create ${PRODUCT_NAME} start=auto DisplayName="${PRODUCT_NAME} Service" binPath= "\"$INSTDIR\${PRODUCT_NAME}.exe\" --service"' - nsExec::Exec 'netsh advfirewall firewall add rule name="${PRODUCT_NAME} Service" dir=in action=allow program="$INSTDIR\${PRODUCT_NAME}.exe" enable=yes' - nsExec::Exec 'sc start ${PRODUCT_NAME}' -SectionEnd - -#################################################################### -# Functions - -Function .onInit - # RustDesk is 64-bit only - ${IfNot} ${RunningX64} - MessageBox MB_ICONSTOP "${PRODUCT_NAME} is 64-bit only!" - Quit - ${EndIf} - ${DisableX64FSRedirection} - SetRegView 64 - - !insertmacro MUI_LANGDLL_DISPLAY -FunctionEnd - -Function CreateDesktopShortcut - CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" -FunctionEnd From ad1ed132d13077f5eebcc24b122be23a9cb21b6d Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:54:00 +0800 Subject: [PATCH 476/506] fix: file transfer, web (#12565) Signed-off-by: fufesou --- flutter/lib/models/file_model.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index fabbcc00c8d..db9b13e45ff 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -30,15 +30,17 @@ enum SortBy { class JobID { int _count = 0; int next() { - String v = bind.mainGetCommonSync(key: 'transfer-job-id'); try { - return int.parse(v); + if (!isWeb) { + String v = bind.mainGetCommonSync(key: 'transfer-job-id'); + return int.parse(v); + } } catch (e) { - // unreachable. But we still handle it to make it safe. - // If we return -1, we have to check it in the caller. - _count++; - return _count; + debugPrint("Failed to get transfer job id: $e"); } + // Finally increase the count if on the web or if failed to get the id. + _count++; + return _count; } } From f6af59b04459e49e6d15c2e0d4585a119b877c14 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 9 Aug 2025 23:26:33 +0800 Subject: [PATCH 477/506] remove useless selfhost job --- .github/workflows/flutter-build.yml | 131 ---------------------------- libs/hbb_common | 2 +- 2 files changed, 1 insertion(+), 132 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 3a7a5e82642..d1eab89bb10 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -424,80 +424,6 @@ jobs: files: | ./SignOutput/rustdesk-*.exe - build-for-macOS-arm64-selfhost: - # 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 - with: - script: | - core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - - - name: Checkout source code - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Build rustdesk - run: | - ./build.py --flutter --hwcodec --unix-file-copy-paste - - - name: create unsigned dmg - if: env.UPLOAD_ARTIFACT == 'true' - run: | - CREATE_DMG="$(command -v create-dmg)" - CREATE_DMG="$(readlink -f "$CREATE_DMG")" - sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG" - create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-arm64.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app - - - name: Upload unsigned macOS app - if: env.UPLOAD_ARTIFACT == 'true' - uses: actions/upload-artifact@master - with: - name: rustdesk-unsigned-macos-arm64 - path: rustdesk-${{ env.VERSION }}-arm64.dmg # can not upload the directory directly or tar.gz file, which destroy the link structure, causing the codesign failed - - - name: Codesign app and create signed dmg - if: env.MACOS_P12_BASE64 != null && env.UPLOAD_ARTIFACT == 'true' - run: | - # Patch create-dmg to give more attempts to unmount image - CREATE_DMG="$(command -v create-dmg)" - CREATE_DMG="$(readlink -f "$CREATE_DMG")" - sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG" - # start sign the rustdesk.app and dmg - rm -rf *.dmg || true - codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv - create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app - codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv - # notarize the rustdesk-${{ env.VERSION }}.dmg - rcodesign notary-submit --api-key-path ~/.p12/api-key.json --staple rustdesk-${{ env.VERSION }}.dmg - - - name: Rename rustdesk - if: env.UPLOAD_ARTIFACT == 'true' - run: | - for name in rustdesk*??.dmg; do - mv "$name" "${name%%.dmg}-aarch64.dmg" - done - - - name: Publish DMG package - if: env.UPLOAD_ARTIFACT == 'true' - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - rustdesk*-aarch64.dmg - build-rustdesk-ios: if: ${{ inputs.upload-artifact }} name: build rustdesk ios ipa @@ -617,63 +543,6 @@ jobs: # files: | # flutter/build/ios/ipa/*.ipa - build-rustdesk-ios-selfhost: - #if: ${{ inputs.upload-artifact }} - if: false - runs-on: [self-hosted, macOS, ARM64] - needs: [generate-bridge] - strategy: - fail-fast: false - steps: - - name: Export GitHub Actions cache environment variables - uses: actions/github-script@v6 - with: - script: | - core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - - - name: Checkout source code - uses: actions/checkout@v4 - with: - submodules: recursive - - # $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 - path: ./ - - - name: Build rustdesk lib - run: | - cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib - - - name: Build rustdesk - # ios sdk not installed on this machine, I will install it later after I am back home - if: false - shell: bash - run: | - pushd flutter - # flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign - # for easy debugging - flutter build ipa --release --no-codesign - - # - 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: flutter/build/ios/ipa/*.ipa - - # - name: Publish ipa 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: | - # flutter/build/ios/ipa/*.ipa build-for-macOS: name: ${{ matrix.job.target }} diff --git a/libs/hbb_common b/libs/hbb_common index 57c8a23ab97..f850a167ac4 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 57c8a23ab970587ea6380943b04dc354020bbe7c +Subproject commit f850a167ac403444451cf90c64d39fa6d3a58e1a From fdb8b498cbe151a8ae254ffbdd6ffabceeec0eb3 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 9 Aug 2025 23:27:56 +0800 Subject: [PATCH 478/506] all use macos-13 --- .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 d1eab89bb10..397e2097226 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -561,7 +561,7 @@ jobs: } - { target: aarch64-apple-darwin, - os: macos-latest, + os: macos-13, # 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, From 302dad2016b519c42fdedf2ae6979cf3842ed41c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 9 Aug 2025 23:46:51 +0800 Subject: [PATCH 479/506] update hbb_common --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index f850a167ac4..32fed54062c 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit f850a167ac403444451cf90c64d39fa6d3a58e1a +Subproject commit 32fed54062c1cdf18146899515ed2850f6ff986b From 43ec57c7691152aad708f49f0a838da87db31e39 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 9 Aug 2025 23:47:19 +0800 Subject: [PATCH 480/506] Feat: file transfer, resume (#12557) Signed-off-by: fufesou --- src/client/io_loop.rs | 63 ++++++++++++++++++++++++++++------------ src/ipc.rs | 5 +++- src/server/connection.rs | 6 +++- src/ui_cm_interface.rs | 8 +++++ 4 files changed, 61 insertions(+), 21 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 4de0e7e3293..9ed96365dac 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -703,6 +703,7 @@ impl Remote { if is_remote { if let Some(job) = get_job(id, &mut self.write_jobs) { job.is_last_job = false; + job.is_resume = true; allow_err!( peer.send(&fs::new_send( id, @@ -717,12 +718,13 @@ impl Remote { } else { if let Some(job) = get_job(id, &mut self.read_jobs) { match &job.data_source { - fs::DataSource::FilePath(p) => { + fs::DataSource::FilePath(_p) => { job.is_last_job = false; + job.is_resume = true; allow_err!( peer.send(&fs::new_receive( id, - p.to_string_lossy().to_string(), + job.remote.clone(), job.file_num, job.files.clone(), job.total_size(), @@ -770,7 +772,8 @@ impl Remote { Some(file_transfer_send_confirm_request::Union::Skip(true)) }, ..Default::default() - }); + }) + .await; } } else { if let Some(job) = fs::get_job(id, &mut self.write_jobs) { @@ -789,7 +792,7 @@ impl Remote { }, ..Default::default() }; - job.confirm(&req); + job.confirm(&req).await; file_action.set_send_confirm(req); msg.set_file_action(file_action); allow_err!(peer.send(&msg).await); @@ -1470,14 +1473,24 @@ impl Remote { if let fs::DataSource::FilePath(p) = &job.data_source { let read_path = get_string(&fs::TransferJob::join(p, &file.name)); - let overwrite_strategy = + let mut overwrite_strategy = job.default_overwrite_strategy(); + let mut offset = 0; + if digest.is_identical && job.is_resume { + if digest.transferred_size > 0 { + overwrite_strategy = Some(true); + offset = digest.transferred_size as _; + } else { + // Force skip if the file is identical and the job is set to resume. + overwrite_strategy = Some(false); + } + } if let Some(overwrite) = overwrite_strategy { let req = FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) + file_transfer_send_confirm_request::Union::OffsetBlk(offset) } else { file_transfer_send_confirm_request::Union::Skip( true, @@ -1485,7 +1498,7 @@ impl Remote { }), ..Default::default() }; - job.confirm(&req); + job.confirm(&req).await; let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } else { @@ -1506,8 +1519,7 @@ impl Remote { if let fs::DataSource::FilePath(p) = &job.data_source { let write_path = get_string(&fs::TransferJob::join(p, &file.name)); - let overwrite_strategy = - job.default_overwrite_strategy(); + job.set_digest(digest.file_size, digest.last_modified); match fs::is_write_need_confirmation( &write_path, &digest, @@ -1515,16 +1527,29 @@ impl Remote { Ok(res) => match res { DigestCheckResult::IsSame => { let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::Skip(true)), - ..Default::default() - }; - job.confirm(&req); + id: digest.id, + file_num: digest.file_num, + union: Some(file_transfer_send_confirm_request::Union::Skip(true)), + ..Default::default() + }; + job.confirm(&req).await; let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } DigestCheckResult::NeedConfirm(digest) => { + let mut overwrite_strategy = + job.default_overwrite_strategy(); + let mut offset = 0; + if digest.is_identical && job.is_resume { + if digest.transferred_size > 0 { + overwrite_strategy = Some(true); + offset = + digest.transferred_size as _; + } else { + // Force skip if the file is identical and the job is set to resume. + overwrite_strategy = Some(false); + } + } if let Some(overwrite) = overwrite_strategy { let req = @@ -1532,13 +1557,13 @@ impl Remote { id: digest.id, file_num: digest.file_num, union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) + file_transfer_send_confirm_request::Union::OffsetBlk(offset) } else { file_transfer_send_confirm_request::Union::Skip(true) }), ..Default::default() }; - job.confirm(&req); + job.confirm(&req).await; let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } else { @@ -1558,7 +1583,7 @@ impl Remote { union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), ..Default::default() }; - job.confirm(&req); + job.confirm(&req).await; let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } @@ -1905,7 +1930,7 @@ impl Remote { }, Some(file_action::Union::SendConfirm(c)) => { if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { - job.confirm(&c); + job.confirm(&c).await; } } _ => {} diff --git a/src/ipc.rs b/src/ipc.rs index 1ae0481622f..8967b92137c 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -26,7 +26,9 @@ use hbb_common::{ config::{self, keys::OPTION_ALLOW_WEBSOCKET, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, password_security as password, timeout, + log, + message_proto::FileTransferSendConfirmRequest, + password_security as password, timeout, tokio::{ self, io::{AsyncRead, AsyncWrite}, @@ -105,6 +107,7 @@ pub enum FS { last_modified: u64, is_upload: bool, }, + SendConfirm(Vec), Rename { id: i32, path: String, diff --git a/src/server/connection.rs b/src/server/connection.rs index 01d84437db0..8ce09f932fc 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2703,7 +2703,11 @@ impl Connection { } Some(file_action::Union::SendConfirm(r)) => { if let Some(job) = fs::get_job(r.id, &mut self.read_jobs) { - job.confirm(&r); + job.confirm(&r).await; + } else { + if let Ok(sc) = r.write_to_bytes() { + self.send_fs(ipc::FS::SendConfirm(sc)); + } } } Some(file_action::Union::Rename(r)) => { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 880f0ca61df..0f5bc5709eb 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -880,6 +880,7 @@ async fn handle_fs( let path = get_string(&fs::TransferJob::join(p, &file.name)); match is_write_need_confirmation(&path, &digest) { Ok(digest_result) => { + job.set_digest(file_size, last_modified); match digest_result { DigestCheckResult::IsSame => { req.set_skip(true); @@ -909,6 +910,13 @@ async fn handle_fs( } } } + ipc::FS::SendConfirm(bytes) => { + if let Ok(r) = FileTransferSendConfirmRequest::parse_from_bytes(&bytes) { + if let Some(job) = fs::get_job(r.id, write_jobs) { + job.confirm(&r).await; + } + } + } ipc::FS::Rename { id, path, new_name } => { rename_file(path, new_name, id, tx).await; } From 4263643200e6605703148553e839a42d4f1f524c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 10 Aug 2025 00:03:45 +0800 Subject: [PATCH 481/506] macos-14 for arm --- .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 397e2097226..a59c3c72243 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -561,7 +561,7 @@ jobs: } - { target: aarch64-apple-darwin, - os: macos-13, + os: macos-14, # 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, From 195479080800fc5900b499c6bc722c50f3d8c960 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sun, 10 Aug 2025 17:44:36 +0800 Subject: [PATCH 482/506] try tcp and udp both --- src/client.rs | 80 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/src/client.rs b/src/client.rs index e13ccff1141..7129e7d9d08 100644 --- a/src/client.rs +++ b/src/client.rs @@ -279,10 +279,10 @@ impl Client { } let (stop_udp_tx, stop_udp_rx) = oneshot::channel::<()>(); - let mut udp = + let udp = // no need to care about multiple rendezvous servers case, since it is acutally not used any more. // Shared state for UDP NAT test result - if crate::get_udp_punch_enabled() { + if crate::get_udp_punch_enabled() && !interface.is_force_relay() { if let Ok((socket, addr)) = new_direct_udp_for(&rendezvous_server).await { let udp_port = Arc::new(Mutex::new(0)); let up_cloned = udp_port.clone(); @@ -298,6 +298,57 @@ impl Client { } else { (None, None) }; + let fut = Self::_start_inner( + peer.to_owned(), + key.to_owned(), + token.to_owned(), + conn_type, + interface.clone(), + udp.clone(), + Some(stop_udp_tx), + rendezvous_server.clone(), + servers.clone(), + contained, + ); + if udp.0.is_none() { + return fut.await; + } + let mut connect_futures = Vec::new(); + connect_futures.push(fut.boxed()); + let fut = Self::_start_inner( + peer.to_owned(), + key.to_owned(), + token.to_owned(), + conn_type, + interface, + (None, None), + None, + rendezvous_server, + servers, + contained, + ); + connect_futures.push(fut.boxed()); + match select_ok(connect_futures).await { + Ok(conn) => Ok((conn.0 .0, conn.0 .1)), + Err(e) => Err(e), + } + } + + async fn _start_inner( + peer: String, + key: String, + token: String, + conn_type: ConnType, + interface: impl Interface, + mut udp: (Option>, Option>>), + stop_udp_tx: Option>, + mut rendezvous_server: String, + servers: Vec, + contained: bool, + ) -> ResultType<( + (Stream, bool, Option>, Option), + (i32, String), + )> { let mut start = Instant::now(); let mut socket = connect_tcp(&*rendezvous_server, CONNECT_TIMEOUT).await; debug_assert!(!servers.contains(&rendezvous_server)); @@ -327,9 +378,8 @@ impl Client { let my_nat_type = crate::get_nat_type(100).await; let mut is_local = false; let mut feedback = 0; - let force_relay = interface.is_force_relay() || use_ws() || Config::is_proxy(); use hbb_common::protobuf::Enum; - let nat_type = if force_relay { + let nat_type = if interface.is_force_relay() { NatType::SYMMETRIC } else { NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT) @@ -337,7 +387,7 @@ impl Client { if !key.is_empty() && !token.is_empty() { // mainly for the security of token - secure_tcp(&mut socket, key) + secure_tcp(&mut socket, &key) .await .map_err(|e| anyhow!("Failed to secure tcp: {}", e))?; } else if let Some(udp) = udp.1.as_ref() { @@ -355,7 +405,7 @@ impl Client { } } // Stop UDP NAT test task if still running - let _ = stop_udp_tx.send(()); + stop_udp_tx.map(|tx| tx.send(())); let mut msg_out = RendezvousMessage::new(); let mut ipv6 = if crate::get_ipv6_punch_enabled() { if let Some((socket, addr)) = crate::get_ipv6_socket().await { @@ -375,7 +425,7 @@ impl Client { conn_type: conn_type.into(), version: crate::VERSION.to_owned(), udp_port: udp_nat_port as _, - force_relay, + force_relay: interface.is_force_relay(), socket_addr_v6: ipv6.1.unwrap_or_default(), ..Default::default() }); @@ -454,10 +504,10 @@ impl Client { } signed_id_pk = rr.pk().into(); let fut = Self::create_relay( - peer, + &peer, rr.uuid, rr.relay_server, - key, + &key, conn_type, my_addr.is_ipv4(), ); @@ -478,7 +528,7 @@ impl Client { feedback = rr.feedback; log::info!("{:?} used to establish {typ} connection", start.elapsed()); let pk = - Self::secure_connection(peer, signed_id_pk, key, &mut conn).await?; + Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?; return Ok(((conn, false, pk, kcp), (feedback, rendezvous_server))); } _ => { @@ -506,7 +556,7 @@ impl Client { Self::connect( my_addr, peer_addr, - peer, + &peer, signed_id_pk, &relay_server, &rendezvous_server, @@ -514,8 +564,8 @@ impl Client { peer_nat_type, my_nat_type, is_local, - key, - token, + &key, + &token, conn_type, interface, udp.0, @@ -1731,7 +1781,9 @@ impl LoginConfigHandler { self.restarting_remote_device = false; self.force_relay = config::option2bool("force-always-relay", &self.get_option("force-always-relay")) - || force_relay; + || force_relay + || use_ws() + || Config::is_proxy(); if let Some((real_id, server, key)) = &self.other_server { let other_server_key = self.get_option("other-server-key"); if !other_server_key.is_empty() && key.is_empty() { From 77064cc2f8c359c0af69a51c5828826c626f802b Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 10 Aug 2025 17:50:25 +0800 Subject: [PATCH 483/506] fix ci --- src/platform/macos.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index c525af7492c..f0ff5cb5850 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -793,6 +793,7 @@ pub fn extract_update_dmg(file: &str) { } } let evt = serde_json::ser::to_string(&evt).unwrap_or("".to_owned()); + #[cfg(feature = "flutter")] crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, evt); } From a0659a277a63117b81cb38aee632f548cb42077a Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 11 Aug 2025 16:13:31 +0800 Subject: [PATCH 484/506] show TCP/UDP/IPv6 in tooltip (#12613) * add punch type log Signed-off-by: 21pages * show TCP/UDP/IPv6 in tooltip Signed-off-by: 21pages * Skip udp punch if udp nat port is 0 Signed-off-by: 21pages --------- Signed-off-by: 21pages --- flutter/lib/common.dart | 21 ++++++ flutter/lib/common/shared_state.dart | 9 ++- .../lib/desktop/pages/remote_tab_page.dart | 12 +--- .../desktop/pages/view_camera_tab_page.dart | 12 +--- flutter/lib/mobile/pages/remote_page.dart | 9 ++- .../lib/mobile/pages/view_camera_page.dart | 8 ++- flutter/lib/models/model.dart | 33 +++++++-- src/client.rs | 68 ++++++++++++++++--- src/client/io_loop.rs | 5 +- src/flutter.rs | 3 +- src/port_forward.rs | 2 +- src/ui/header.tis | 10 ++- src/ui/remote.rs | 7 +- src/ui_session_interface.rs | 9 ++- 14 files changed, 156 insertions(+), 52 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index fda3f84e3ea..c1dd7cd4ec6 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3910,3 +3910,24 @@ String get appName { } return _appName; } + +String getConnectionText(bool secure, bool direct, String streamType) { + String connectionText; + if (secure && direct) { + connectionText = translate("Direct and encrypted connection"); + } else if (secure && !direct) { + connectionText = translate("Relayed and encrypted connection"); + } else if (!secure && direct) { + connectionText = translate("Direct and unencrypted connection"); + } else { + connectionText = translate("Relayed and unencrypted connection"); + } + if (streamType == 'Relay') { + streamType = 'TCP'; + } + if (streamType.isEmpty) { + return connectionText; + } else { + return '$connectionText ($streamType)'; + } +} diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index 908c98a70e3..4f9373ccd4e 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -77,9 +77,11 @@ class CurrentDisplayState { class ConnectionType { final Rx _secure = kInvalidValueStr.obs; final Rx _direct = kInvalidValueStr.obs; + final Rx _stream_type = kInvalidValueStr.obs; Rx get secure => _secure; Rx get direct => _direct; + Rx get stream_type => _stream_type; static String get strSecure => 'secure'; static String get strInsecure => 'insecure'; @@ -94,9 +96,14 @@ class ConnectionType { _direct.value = v ? strDirect : strIndirect; } + void setStreamType(String v) { + _stream_type.value = v; + } + bool isValid() { return _secure.value != kInvalidValueStr && - _direct.value != kInvalidValueStr; + _direct.value != kInvalidValueStr && + _stream_type.value != kInvalidValueStr; } } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 644f6c3367a..ba698bd56b7 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -146,16 +146,8 @@ class _ConnectionTabPageState extends State { connectionType.secure.value == ConnectionType.strSecure; bool direct = connectionType.direct.value == ConnectionType.strDirect; - String msgConn; - if (secure && direct) { - msgConn = translate("Direct and encrypted connection"); - } else if (secure && !direct) { - msgConn = translate("Relayed and encrypted connection"); - } else if (!secure && direct) { - msgConn = translate("Direct and unencrypted connection"); - } else { - msgConn = translate("Relayed and unencrypted connection"); - } + String msgConn = getConnectionText( + secure, direct, connectionType.stream_type.value); var msgFingerprint = '${translate('Fingerprint')}:\n'; var fingerprint = FingerprintState.find(key).value; if (fingerprint.isEmpty) { diff --git a/flutter/lib/desktop/pages/view_camera_tab_page.dart b/flutter/lib/desktop/pages/view_camera_tab_page.dart index 4510949fa25..a31ba0fffc0 100644 --- a/flutter/lib/desktop/pages/view_camera_tab_page.dart +++ b/flutter/lib/desktop/pages/view_camera_tab_page.dart @@ -145,16 +145,8 @@ class _ViewCameraTabPageState extends State { connectionType.secure.value == ConnectionType.strSecure; bool direct = connectionType.direct.value == ConnectionType.strDirect; - String msgConn; - if (secure && direct) { - msgConn = translate("Direct and encrypted connection"); - } else if (secure && !direct) { - msgConn = translate("Relayed and encrypted connection"); - } else if (!secure && direct) { - msgConn = translate("Direct and unencrypted connection"); - } else { - msgConn = translate("Relayed and unencrypted connection"); - } + String msgConn = getConnectionText( + secure, direct, connectionType.stream_type.value); var msgFingerprint = '${translate('Fingerprint')}:\n'; var fingerprint = FingerprintState.find(key).value; if (fingerprint.isEmpty) { diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index b707fd38f0f..4c8081465b2 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -40,7 +40,12 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) { } class RemotePage extends StatefulWidget { - RemotePage({Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay}) + RemotePage( + {Key? key, + required this.id, + this.password, + this.isSharedPassword, + this.forceRelay}) : super(key: key); final String id; @@ -1105,7 +1110,7 @@ void showOptions( BuildContext context, String id, OverlayDialogManager dialogManager) async { var displays = []; final pi = gFFI.ffiModel.pi; - final image = gFFI.ffiModel.getConnectionImage(); + final image = gFFI.ffiModel.getConnectionImageText(); if (image != null) { displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); } diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart index 1b668673ac8..53af56267da 100644 --- a/flutter/lib/mobile/pages/view_camera_page.dart +++ b/flutter/lib/mobile/pages/view_camera_page.dart @@ -39,7 +39,11 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) { class ViewCameraPage extends StatefulWidget { ViewCameraPage( - {Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay}) + {Key? key, + required this.id, + this.password, + this.isSharedPassword, + this.forceRelay}) : super(key: key); final String id; @@ -579,7 +583,7 @@ void showOptions( BuildContext context, String id, OverlayDialogManager dialogManager) async { var displays = []; final pi = gFFI.ffiModel.pi; - final image = gFFI.ffiModel.getConnectionImage(); + final image = gFFI.ffiModel.getConnectionImageText(); if (image != null) { displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c6118efa195..645002686d9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -61,6 +61,7 @@ class CachedPeerData { bool secure = false; bool direct = false; + String streamType = ''; CachedPeerData(); @@ -74,6 +75,7 @@ class CachedPeerData { 'permissions': permissions, 'secure': secure, 'direct': direct, + 'streamType': streamType, }); } @@ -92,6 +94,7 @@ class CachedPeerData { }); data.secure = map['secure']; data.direct = map['direct']; + data.streamType = map['streamType']; return data; } catch (e) { debugPrint('Failed to parse CachedPeerData: $e'); @@ -223,27 +226,45 @@ class FfiModel with ChangeNotifier { timerScreenshot?.cancel(); } - setConnectionType(String peerId, bool secure, bool direct) { + setConnectionType( + String peerId, bool secure, bool direct, String streamType) { cachedPeerData.secure = secure; cachedPeerData.direct = direct; + cachedPeerData.streamType = streamType; _secure = secure; _direct = direct; try { var connectionType = ConnectionTypeState.find(peerId); connectionType.setSecure(secure); connectionType.setDirect(direct); + connectionType.setStreamType(streamType); } catch (e) { // } } - Widget? getConnectionImage() { + Widget? getConnectionImageText() { if (secure == null || direct == null) { return null; } else { final icon = '${secure == true ? 'secure' : 'insecure'}${direct == true ? '' : '_relay'}'; - return SvgPicture.asset('assets/$icon.svg', width: 48, height: 48); + final iconWidget = + SvgPicture.asset('assets/$icon.svg', width: 48, height: 48); + String connectionText = + getConnectionText(secure!, direct!, cachedPeerData.streamType); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + iconWidget, + SizedBox(height: 4), + Text( + connectionText, + style: TextStyle(fontSize: 12), + textAlign: TextAlign.center, + ), + ], + ); } } @@ -260,7 +281,7 @@ class FfiModel with ChangeNotifier { 'link': '', }, sessionId, peerId); updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId); - setConnectionType(peerId, data.secure, data.direct); + setConnectionType(peerId, data.secure, data.direct, data.streamType); await handlePeerInfo(data.peerInfo, peerId, true); for (final element in data.cursorDataList) { updateLastCursorId(element); @@ -289,8 +310,8 @@ class FfiModel with ChangeNotifier { } else if (name == 'sync_platform_additions') { handlePlatformAdditions(evt, sessionId, peerId); } else if (name == 'connection_ready') { - setConnectionType( - peerId, evt['secure'] == 'true', evt['direct'] == 'true'); + setConnectionType(peerId, evt['secure'] == 'true', + evt['direct'] == 'true', evt['stream_type'] ?? ''); } else if (name == 'switch_display') { // switch display is kept for backward compatibility handleSwitchDisplay(evt, sessionId, peerId); diff --git a/src/client.rs b/src/client.rs index 7129e7d9d08..438c2ecc69f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -192,7 +192,13 @@ impl Client { conn_type: ConnType, interface: impl Interface, ) -> ResultType<( - (Stream, bool, Option>, Option), + ( + Stream, + bool, + Option>, + Option, + &'static str, + ), (i32, String), )> { debug_assert!(peer == interface.get_id()); @@ -219,7 +225,13 @@ impl Client { conn_type: ConnType, interface: impl Interface, ) -> ResultType<( - (Stream, bool, Option>, Option), + ( + Stream, + bool, + Option>, + Option, + &'static str, + ), (i32, String), )> { if config::is_incoming_only() { @@ -234,6 +246,7 @@ impl Client { true, None, None, + "TCP", ), (0, "".to_owned()), )); @@ -246,6 +259,7 @@ impl Client { true, None, None, + "TCP", ), (0, "".to_owned()), )); @@ -257,7 +271,7 @@ impl Client { } else { (peer, "", key, token) }; - let (mut rendezvous_server, servers, contained) = if other_server.is_empty() { + let (rendezvous_server, servers, contained) = if other_server.is_empty() { crate::get_rendezvous_server(1_000).await } else { if other_server == PUBLIC_SERVER { @@ -346,7 +360,13 @@ impl Client { servers: Vec, contained: bool, ) -> ResultType<( - (Stream, bool, Option>, Option), + ( + Stream, + bool, + Option>, + Option, + &'static str, + ), (i32, String), )> { let mut start = Instant::now(); @@ -417,6 +437,12 @@ impl Client { (None, None) }; let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0); + if udp.0.is_some() && udp_nat_port == 0 { + let err_msg = "skip udp punch because udp nat port is 0"; + log::info!("{}", err_msg); + bail!(err_msg); + } + let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" }; msg_out.set_punch_hole_request(PunchHoleRequest { id: peer.to_owned(), token: token.to_owned(), @@ -430,7 +456,13 @@ impl Client { ..Default::default() }); for i in 1..=3 { - log::info!("#{} punch attempt with {}, id: {}", i, my_addr, peer); + log::info!( + "#{} {} punch attempt with {}, id: {}", + i, + punch_type, + my_addr, + peer + ); socket.send(&msg_out).await?; // below timeout should not bigger than hbbs's connection timeout. if let Some(msg_in) = @@ -481,7 +513,7 @@ impl Client { } } } - log::info!("Hole Punched {} = {}", peer, peer_addr); + log::info!("{} Hole Punched {} = {}", punch_type, peer, peer_addr); break; } } @@ -529,7 +561,7 @@ impl Client { log::info!("{:?} used to establish {typ} connection", start.elapsed()); let pk = Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?; - return Ok(((conn, false, pk, kcp), (feedback, rendezvous_server))); + return Ok(((conn, false, pk, kcp, typ), (feedback, rendezvous_server))); } _ => { log::error!("Unexpected protobuf msg received: {:?}", msg_in); @@ -543,8 +575,9 @@ impl Client { } let time_used = start.elapsed().as_millis() as u64; log::info!( - "{} ms used to punch hole, relay_server: {}, {}", + "{} ms used to {} punch hole, relay_server: {}, {}", time_used, + punch_type, relay_server, if is_local { "is_local: true".to_owned() @@ -570,6 +603,7 @@ impl Client { interface, udp.0, ipv6.0, + punch_type, ) .await?, (feedback, rendezvous_server), @@ -594,7 +628,14 @@ impl Client { interface: impl Interface, udp_socket_nat: Option>, udp_socket_v6: Option>, - ) -> ResultType<(Stream, bool, Option>, Option)> { + punch_type: &str, + ) -> ResultType<( + Stream, + bool, + Option>, + Option, + &'static str, + )> { let direct_failures = interface.get_lch().read().unwrap().direct_failures; let mut connect_timeout = 0; const MIN: u64 = 1000; @@ -681,9 +722,14 @@ impl Client { interface.get_lch().write().unwrap().set_direct_failure(n); } let mut conn = conn?; - log::info!("{:?} used to establish {typ} connection", start.elapsed()); + log::info!( + "{:?} used to establish {typ} connection with {} punch", + start.elapsed(), + punch_type + ); let pk = Self::secure_connection(peer_id, signed_id_pk, key, &mut conn).await?; - Ok((conn, direct, pk, kcp)) + log::info!("{} punch secure_connection ok", punch_type); + Ok((conn, direct, pk, kcp, typ)) } /// Establish secure connection with the server. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 9ed96365dac..878a227f230 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -174,13 +174,14 @@ impl Remote { ) .await { - Ok(((mut peer, direct, pk, kcp), (feedback, rendezvous_server))) => { + Ok(((mut peer, direct, pk, kcp, stream_type), (feedback, rendezvous_server))) => { self.handler .connection_round_state .lock() .unwrap() .set_connected(); - self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready + self.handler + .set_connection_type(peer.is_secured(), direct, stream_type); // flutter -> connection_ready self.handler.update_direct(Some(direct)); if conn_type == ConnType::DEFAULT_CONN || conn_type == ConnType::VIEW_CAMERA { self.handler diff --git a/src/flutter.rs b/src/flutter.rs index 602f5701a81..198d685051b 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -716,12 +716,13 @@ impl InvokeUiSession for FlutterHandler { ); } - fn set_connection_type(&self, is_secured: bool, direct: bool) { + fn set_connection_type(&self, is_secured: bool, direct: bool, stream_type: &str) { self.push_event( "connection_ready", &[ ("secure", &is_secured.to_string()), ("direct", &direct.to_string()), + ("stream_type", &stream_type.to_string()), ], &[], ); diff --git a/src/port_forward.rs b/src/port_forward.rs index bd4b9fb78b1..056233b00cc 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -118,7 +118,7 @@ async fn connect_and_login( } else { ConnType::PORT_FORWARD }; - let ((mut stream, direct, _pk, _kcp), (feedback, rendezvous_server)) = + let ((mut stream, direct, _pk, _kcp, _stream_type), (feedback, rendezvous_server)) = Client::start(id, key, token, conn_type, interface.clone()).await?; interface.update_direct(Some(direct)); let mut buffer = Vec::new(); diff --git a/src/ui/header.tis b/src/ui/header.tis index 305248b458c..17efe698267 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -117,6 +117,13 @@ class Header: Reactor.Component { icon_conn = svg_insecure_relay; title_conn = translate("Relayed and unencrypted connection"); } + var stream_type = this.stream_type; + if (stream_type == "Relay") { + stream_type = "TCP"; + } + if (stream_type) { + title_conn += " (" + stream_type + ")"; + } var title = get_id(); if (pi.hostname) title += "(" + pi.username + "@" + pi.hostname + ")"; if ((pi.displays || []).length == 0) { @@ -695,10 +702,11 @@ function startChat() { chatbox = view.window(params); } -handler.setConnectionType = function(secured, direct) { +handler.setConnectionType = function(secured, direct, stream_type) { header.update({ secure_connection: secured, direct_connection: direct, + stream_type: stream_type, }); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f99e2de6ee1..f67f3790294 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -178,8 +178,11 @@ impl InvokeUiSession for SciterHandler { self.call("setCursorPosition", &make_args!(cp.x, cp.y)); } - fn set_connection_type(&self, is_secured: bool, direct: bool) { - self.call("setConnectionType", &make_args!(is_secured, direct)); + fn set_connection_type(&self, is_secured: bool, direct: bool, stream_type: &str) { + self.call( + "setConnectionType", + &make_args!(is_secured, direct, stream_type.to_string()), + ); } fn set_fingerprint(&self, _fingerprint: String) {} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 93dde39094a..fcb84da2194 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -192,7 +192,11 @@ impl Session { } pub fn is_default(&self) -> bool { - self.lc.read().unwrap().conn_type.eq(&ConnType::DEFAULT_CONN) + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::DEFAULT_CONN) } pub fn is_view_camera(&self) -> bool { @@ -804,7 +808,6 @@ impl Session { self.send(Data::Message(msg_out)); } - pub fn capture_displays(&self, add: Vec, sub: Vec, set: Vec) { let mut misc = Misc::new(); misc.set_capture_displays(CaptureDisplays { @@ -1611,7 +1614,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_permission(&self, name: &str, value: bool); fn close_success(&self); fn update_quality_status(&self, qs: QualityStatus); - fn set_connection_type(&self, is_secured: bool, direct: bool); + fn set_connection_type(&self, is_secured: bool, direct: bool, stream_type: &str); fn set_fingerprint(&self, fingerprint: String); fn job_error(&self, id: i32, err: String, file_num: i32); fn job_done(&self, id: i32, file_num: i32); From 1fb0123ed7ac59993bcd23ede0a8e1db0c81f861 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 11 Aug 2025 20:41:46 +0800 Subject: [PATCH 485/506] remove skip udp punch if udp nat port is 0 (#12615) Signed-off-by: 21pages --- src/client.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index 438c2ecc69f..ebbea260d08 100644 --- a/src/client.rs +++ b/src/client.rs @@ -437,11 +437,6 @@ impl Client { (None, None) }; let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0); - if udp.0.is_some() && udp_nat_port == 0 { - let err_msg = "skip udp punch because udp nat port is 0"; - log::info!("{}", err_msg); - bail!(err_msg); - } let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" }; msg_out.set_punch_hole_request(PunchHoleRequest { id: peer.to_owned(), From 53efaf125cf15417237bb3b65a52ee23c347cb0c Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Mon, 11 Aug 2025 23:25:41 +0800 Subject: [PATCH 486/506] Revert "Feat: file transfer, resume (#12557)" (#12620) This reverts commit 43ec57c7691152aad708f49f0a838da87db31e39. --- src/client/io_loop.rs | 63 ++++++++++++---------------------------- src/ipc.rs | 5 +--- src/server/connection.rs | 6 +--- src/ui_cm_interface.rs | 8 ----- 4 files changed, 21 insertions(+), 61 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 878a227f230..3b07525fb5d 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -704,7 +704,6 @@ impl Remote { if is_remote { if let Some(job) = get_job(id, &mut self.write_jobs) { job.is_last_job = false; - job.is_resume = true; allow_err!( peer.send(&fs::new_send( id, @@ -719,13 +718,12 @@ impl Remote { } else { if let Some(job) = get_job(id, &mut self.read_jobs) { match &job.data_source { - fs::DataSource::FilePath(_p) => { + fs::DataSource::FilePath(p) => { job.is_last_job = false; - job.is_resume = true; allow_err!( peer.send(&fs::new_receive( id, - job.remote.clone(), + p.to_string_lossy().to_string(), job.file_num, job.files.clone(), job.total_size(), @@ -773,8 +771,7 @@ impl Remote { Some(file_transfer_send_confirm_request::Union::Skip(true)) }, ..Default::default() - }) - .await; + }); } } else { if let Some(job) = fs::get_job(id, &mut self.write_jobs) { @@ -793,7 +790,7 @@ impl Remote { }, ..Default::default() }; - job.confirm(&req).await; + job.confirm(&req); file_action.set_send_confirm(req); msg.set_file_action(file_action); allow_err!(peer.send(&msg).await); @@ -1474,24 +1471,14 @@ impl Remote { if let fs::DataSource::FilePath(p) = &job.data_source { let read_path = get_string(&fs::TransferJob::join(p, &file.name)); - let mut overwrite_strategy = + let overwrite_strategy = job.default_overwrite_strategy(); - let mut offset = 0; - if digest.is_identical && job.is_resume { - if digest.transferred_size > 0 { - overwrite_strategy = Some(true); - offset = digest.transferred_size as _; - } else { - // Force skip if the file is identical and the job is set to resume. - overwrite_strategy = Some(false); - } - } if let Some(overwrite) = overwrite_strategy { let req = FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(offset) + file_transfer_send_confirm_request::Union::OffsetBlk(0) } else { file_transfer_send_confirm_request::Union::Skip( true, @@ -1499,7 +1486,7 @@ impl Remote { }), ..Default::default() }; - job.confirm(&req).await; + job.confirm(&req); let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } else { @@ -1520,7 +1507,8 @@ impl Remote { if let fs::DataSource::FilePath(p) = &job.data_source { let write_path = get_string(&fs::TransferJob::join(p, &file.name)); - job.set_digest(digest.file_size, digest.last_modified); + let overwrite_strategy = + job.default_overwrite_strategy(); match fs::is_write_need_confirmation( &write_path, &digest, @@ -1528,29 +1516,16 @@ impl Remote { Ok(res) => match res { DigestCheckResult::IsSame => { let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::Skip(true)), - ..Default::default() - }; - job.confirm(&req).await; + id: digest.id, + file_num: digest.file_num, + union: Some(file_transfer_send_confirm_request::Union::Skip(true)), + ..Default::default() + }; + job.confirm(&req); let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } DigestCheckResult::NeedConfirm(digest) => { - let mut overwrite_strategy = - job.default_overwrite_strategy(); - let mut offset = 0; - if digest.is_identical && job.is_resume { - if digest.transferred_size > 0 { - overwrite_strategy = Some(true); - offset = - digest.transferred_size as _; - } else { - // Force skip if the file is identical and the job is set to resume. - overwrite_strategy = Some(false); - } - } if let Some(overwrite) = overwrite_strategy { let req = @@ -1558,13 +1533,13 @@ impl Remote { id: digest.id, file_num: digest.file_num, union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(offset) + file_transfer_send_confirm_request::Union::OffsetBlk(0) } else { file_transfer_send_confirm_request::Union::Skip(true) }), ..Default::default() }; - job.confirm(&req).await; + job.confirm(&req); let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } else { @@ -1584,7 +1559,7 @@ impl Remote { union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), ..Default::default() }; - job.confirm(&req).await; + job.confirm(&req); let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } @@ -1931,7 +1906,7 @@ impl Remote { }, Some(file_action::Union::SendConfirm(c)) => { if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { - job.confirm(&c).await; + job.confirm(&c); } } _ => {} diff --git a/src/ipc.rs b/src/ipc.rs index 8967b92137c..1ae0481622f 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -26,9 +26,7 @@ use hbb_common::{ config::{self, keys::OPTION_ALLOW_WEBSOCKET, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, - message_proto::FileTransferSendConfirmRequest, - password_security as password, timeout, + log, password_security as password, timeout, tokio::{ self, io::{AsyncRead, AsyncWrite}, @@ -107,7 +105,6 @@ pub enum FS { last_modified: u64, is_upload: bool, }, - SendConfirm(Vec), Rename { id: i32, path: String, diff --git a/src/server/connection.rs b/src/server/connection.rs index 8ce09f932fc..01d84437db0 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2703,11 +2703,7 @@ impl Connection { } Some(file_action::Union::SendConfirm(r)) => { if let Some(job) = fs::get_job(r.id, &mut self.read_jobs) { - job.confirm(&r).await; - } else { - if let Ok(sc) = r.write_to_bytes() { - self.send_fs(ipc::FS::SendConfirm(sc)); - } + job.confirm(&r); } } Some(file_action::Union::Rename(r)) => { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 0f5bc5709eb..880f0ca61df 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -880,7 +880,6 @@ async fn handle_fs( let path = get_string(&fs::TransferJob::join(p, &file.name)); match is_write_need_confirmation(&path, &digest) { Ok(digest_result) => { - job.set_digest(file_size, last_modified); match digest_result { DigestCheckResult::IsSame => { req.set_skip(true); @@ -910,13 +909,6 @@ async fn handle_fs( } } } - ipc::FS::SendConfirm(bytes) => { - if let Ok(r) = FileTransferSendConfirmRequest::parse_from_bytes(&bytes) { - if let Some(job) = fs::get_job(r.id, write_jobs) { - job.confirm(&r).await; - } - } - } ipc::FS::Rename { id, path, new_name } => { rename_file(path, new_name, id, tx).await; } From d6d44be1b72ac60c8be57fb0cfca4aff4f2a4fb1 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 11 Aug 2025 23:28:19 +0800 Subject: [PATCH 487/506] temporrarily revert file transfer resume --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index 32fed54062c..57c8a23ab97 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 32fed54062c1cdf18146899515ed2850f6ff986b +Subproject commit 57c8a23ab970587ea6380943b04dc354020bbe7c From e7909a0dbd7829cfb568a7435f1b7c8e92004eeb Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 12 Aug 2025 17:48:20 +0800 Subject: [PATCH 488/506] opt update of direct/direct_failures (#12627) Signed-off-by: 21pages --- src/client.rs | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/client.rs b/src/client.rs index ebbea260d08..4b98fb7562e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -204,7 +204,7 @@ impl Client { debug_assert!(peer == interface.get_id()); interface.update_direct(None); interface.update_received(false); - match Self::_start(peer, key, token, conn_type, interface).await { + match Self::_start(peer, key, token, conn_type, interface.clone()).await { Err(err) => { let err_str = err.to_string(); if err_str.starts_with("Failed") { @@ -213,7 +213,16 @@ impl Client { return Err(err); } } - Ok(x) => Ok(x), + Ok(x) => { + let direct_failures = interface.get_lch().read().unwrap().direct_failures; + let direct = x.0 .1; + if !interface.is_force_relay() && (direct_failures == 0) != direct { + let n = if direct { 0 } else { 1 }; + log::info!("direct_failures updated to {}", n); + interface.get_lch().write().unwrap().set_direct_failure(n); + } + Ok(x) + } } } @@ -688,7 +697,6 @@ impl Client { }; let mut direct = !conn.is_err(); - interface.update_direct(Some(direct)); if interface.is_force_relay() || conn.is_err() { if !relay_server.is_empty() { conn = Self::request_relay( @@ -701,8 +709,9 @@ impl Client { conn_type, ) .await; - interface.update_direct(Some(false)); if let Err(e) = conn { + // this direct is mainly used by on_establish_connection_error, so we update it here before bail + interface.update_direct(Some(false)); bail!("Failed to connect via relay server: {}", e); } typ = "Relay"; @@ -711,19 +720,22 @@ impl Client { bail!("Failed to make direct connection to remote desktop"); } } - if !relay_server.is_empty() && (direct_failures == 0) != direct { - let n = if direct { 0 } else { 1 }; - log::info!("direct_failures updated to {}", n); - interface.get_lch().write().unwrap().set_direct_failure(n); - } let mut conn = conn?; log::info!( "{:?} used to establish {typ} connection with {} punch", start.elapsed(), punch_type ); - let pk = Self::secure_connection(peer_id, signed_id_pk, key, &mut conn).await?; - log::info!("{} punch secure_connection ok", punch_type); + let res = Self::secure_connection(peer_id, signed_id_pk, key, &mut conn).await; + let pk: Option> = match res { + Ok(pk) => pk, + Err(e) => { + // this direct is mainly used by on_establish_connection_error, so we update it here before bail + interface.update_direct(Some(direct)); + bail!(e); + } + }; + log::debug!("{} punch secure_connection ok", punch_type); Ok((conn, direct, pk, kcp, typ)) } From 806351b6c1165232b169af5340652a708ec9dde7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 12 Aug 2025 20:29:48 +0800 Subject: [PATCH 489/506] fix remote tab tooltip (#12632) Signed-off-by: 21pages --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 4b98fb7562e..624e9a51737 100644 --- a/src/client.rs +++ b/src/client.rs @@ -550,7 +550,7 @@ impl Client { connect_futures.push( async move { let conn = fut.await?; - Ok((conn, None, "Relay")) + Ok((conn, None, if use_ws() { "WebSocket" } else { "Relay" })) } .boxed(), ); From 03e2d017f0a0d536dcf5d4bdd12dbdb7a9a9ca27 Mon Sep 17 00:00:00 2001 From: Matthias Weiss Date: Tue, 12 Aug 2025 14:47:39 +0200 Subject: [PATCH 490/506] Remove unused jobs --- .github/workflows/flutter-build.yml | 39 ++++--------- .github/workflows/playground.yml | 14 ++--- .../third-party-RustDeskTempTopMostWindow.yml | 55 ++++++++++--------- 3 files changed, 45 insertions(+), 63 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 3a7a5e82642..17ffd4edb58 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -34,7 +34,7 @@ env: # vcpkg version: 2025.01.13 # If we change the `VCPKG COMMIT_ID`, please remember: # 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`. - # Or we may face build issue like + # Or we may face build issue like # https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174 # 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`. VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" @@ -51,6 +51,7 @@ jobs: uses: ./.github/workflows/bridge.yml build-RustDeskTempTopMostWindow: + if: false uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml with: upload-artifact: ${{ inputs.upload-artifact }} @@ -62,6 +63,7 @@ jobs: fail-fast: false build-for-windows-flutter: + if: false name: ${{ matrix.job.target }} needs: [build-RustDeskTempTopMostWindow, generate-bridge] runs-on: ${{ matrix.job.os }} @@ -282,6 +284,7 @@ jobs: # The fallback for the flutter version, we use Sciter for 32bit Windows. build-for-windows-sciter: + if: false name: ${{ matrix.job.target }} (${{ matrix.job.os }}) runs-on: ${{ matrix.job.os }} # Temporarily disable this action due to additional test is needed. @@ -499,7 +502,8 @@ jobs: rustdesk*-aarch64.dmg build-rustdesk-ios: - if: ${{ inputs.upload-artifact }} + if: false + # if: ${{ inputs.upload-artifact }} name: build rustdesk ios ipa runs-on: ${{ matrix.job.os }} needs: [generate-bridge] @@ -586,7 +590,7 @@ 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: @@ -676,6 +680,7 @@ jobs: # flutter/build/ios/ipa/*.ipa build-for-macOS: + if: false name: ${{ matrix.job.target }} runs-on: ${{ matrix.job.os }} needs: [generate-bridge] @@ -919,6 +924,7 @@ jobs: files: rustdesk-${{ env.VERSION }}-unsigned.tar.gz build-rustdesk-android: + if: false needs: [generate-bridge] name: build rustdesk android apk ${{ matrix.job.target }} runs-on: ${{ matrix.job.os }} @@ -1209,6 +1215,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 if: ${{ inputs.upload-artifact }} @@ -1407,14 +1414,6 @@ jobs: deb_arch: amd64, vcpkg-triplet: x64-linux, } - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - distro: ubuntu18.04, - on: ubuntu-22.04-arm, - deb_arch: arm64, - vcpkg-triplet: arm64-linux, - } steps: - name: Export GitHub Actions cache environment variables uses: actions/github-script@v6 @@ -1745,16 +1744,6 @@ jobs: vcpkg-triplet: x64-linux, extra_features: ",hwcodec,unix-file-copy-paste", } - - { - arch: armv7, - target: armv7-unknown-linux-gnueabihf, - on: ubuntu-22.04-arm, - distro: ubuntu18.04-rustdesk, - deb_arch: armhf, - sciter_arch: arm32, - vcpkg-triplet: arm-linux, - extra_features: ",unix-file-copy-paste", - } steps: - name: Export GitHub Actions cache environment variables uses: actions/github-script@v6 @@ -2019,14 +2008,6 @@ jobs: arch: x86_64, suffix: "-sciter", } - - { - 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: ubuntu-22.04-arm, - arch: aarch64, - suffix: "", - } steps: - name: Checkout source code uses: actions/checkout@v4 diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 53e7f642ff4..b1c1c2b306a 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -2,7 +2,7 @@ name: playground on: #schedule: - # schedule build every night + # schedule build every night # - cron: "0/6 * * * *" workflow_dispatch: @@ -31,6 +31,7 @@ env: jobs: build-for-macOS: + if: false name: ${{ matrix.job.target }} runs-on: ${{ matrix.job.os }} strategy: @@ -45,7 +46,7 @@ jobs: flutter: "3.13.9", ref: "f6509e3fd6917aa976bad2fc684182601ebf2434", bridge: "1.80.1", - date: "20231219" + date: "20231219", } - { target: x86_64-apple-darwin, @@ -55,7 +56,7 @@ jobs: flutter: "3.10.6", ref: "f6509e3fd6917aa976bad2fc684182601ebf2434", bridge: "1.80.1", - date: "20231219" + date: "20231219", } - { target: x86_64-apple-darwin, @@ -65,7 +66,7 @@ jobs: flutter: "3.10.6", ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8", bridge: "1.80.1", - date: "20231119" + date: "20231119", } - { target: x86_64-apple-darwin, @@ -75,7 +76,7 @@ jobs: flutter: "3.13.9", ref: "85ddfc0739f052cab0029c46b899b959ee94eeb8", bridge: "1.80.1", - date: "20231119" + date: "20231119", } steps: - name: Export GitHub Actions cache environment variables @@ -163,7 +164,7 @@ jobs: - name: Install vcpkg dependencies run: | $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed" - + - name: Restore from cache and install vcpkg uses: lukka/run-vcpkg@v7 if: false @@ -229,7 +230,6 @@ jobs: files: | rustdesk*-${{ matrix.job.arch }}*.dmg - build-rustdesk-android: if: false name: build rustdesk android apk ${{ matrix.job.target }} diff --git a/.github/workflows/third-party-RustDeskTempTopMostWindow.yml b/.github/workflows/third-party-RustDeskTempTopMostWindow.yml index 2f89092b76b..ff8560950fc 100644 --- a/.github/workflows/third-party-RustDeskTempTopMostWindow.yml +++ b/.github/workflows/third-party-RustDeskTempTopMostWindow.yml @@ -3,38 +3,39 @@ name: build RustDeskTempTopMostWindow on: workflow_call: inputs: - upload-artifact: - type: boolean - default: true - target: - description: 'Target' - required: true - type: string - default: 'windows-2022' - configuration: - description: 'Configuration' - required: true - type: string - default: 'Release' - platform: - description: 'Platform' - required: true - type: string - default: 'x64' - target_version: - description: 'TargetVersion' - required: true - type: string - default: 'Windows10' + upload-artifact: + type: boolean + default: true + target: + description: "Target" + required: true + type: string + default: "windows-2022" + configuration: + description: "Configuration" + required: true + type: string + default: "Release" + platform: + description: "Platform" + required: true + type: string + default: "x64" + target_version: + description: "TargetVersion" + required: true + type: string + default: "Windows10" env: project_path: WindowInjection/WindowInjection.vcxproj jobs: build-RustDeskTempTopMostWindow: + if: false runs-on: ${{ inputs.target }} strategy: - fail-fast: false + fail-fast: false env: build_output_dir: RustDeskTempTopMostWindow/WindowInjection/${{ inputs.platform }}/${{ inputs.configuration }} steps: @@ -55,6 +56,6 @@ jobs: uses: actions/upload-artifact@master if: ${{ inputs.upload-artifact }} with: - name: topmostwindow-artifacts - path: | - ./${{ env.build_output_dir }}/WindowInjection.dll + name: topmostwindow-artifacts + path: | + ./${{ env.build_output_dir }}/WindowInjection.dll From 47dc58894ef90c65720ee8e320d8900e9b1c8909 Mon Sep 17 00:00:00 2001 From: Matthias Weiss Date: Tue, 12 Aug 2025 14:50:36 +0200 Subject: [PATCH 491/506] Remove update box --- .../lib/desktop/pages/desktop_home_page.dart | 22 ------- src/ui/index.tis | 62 +++++++++---------- 2 files changed, 30 insertions(+), 54 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 0f302d8e148..d71557fee1b 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -429,28 +429,6 @@ class _DesktopHomePageState extends State } Widget buildHelpCards(String updateUrl) { - if (!bind.isCustomClient() && - updateUrl.isNotEmpty && - !isCardClosed && - bind.mainUriPrefixSync().contains('rustdesk')) { - final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled(); - String btnText = isToUpdate ? 'Click to update' : 'Click to download'; - GestureTapCallback onPressed = () async { - final Uri url = Uri.parse('https://rustdesk.com/download'); - await launchUrl(url); - }; - if (isToUpdate) { - onPressed = () { - handleUpdate(updateUrl); - }; - } - return buildInstallCard( - "Status", - "${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).", - btnText, - onPressed, - closeButton: true); - } if (systemError.isNotEmpty) { return buildInstallCard("", systemError, "", () {}); } diff --git a/src/ui/index.tis b/src/ui/index.tis index bee8bba8c53..605da7b0005 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -64,7 +64,7 @@ function createNewConnect(id, type) { if (!id) return; var old_id = id; id = handler.handle_relay_id(id); - var force_relay = old_id != id; + var force_relay = old_id != id; if (id == my_id) { msgbox("custom-error", "Error", "You cannot connect to your own computer"); return; @@ -79,7 +79,7 @@ class ShareRdp: Reactor.Component { var cls = handler.is_share_rdp() ? "selected" : "line-through"; return
  • {svg_checkmark}{rdp_shared_string}
  • ; } - + function onClick() { handler.set_share_rdp(!handler.is_share_rdp()); this.update(); @@ -98,7 +98,7 @@ class DirectServer: Reactor.Component { var cls = enabled ? "selected" : "line-through"; return
  • {svg_checkmark}{text}{enabled && }
  • ; } - + function onClick() { if (is_edit_rdp_port) { is_edit_rdp_port = false; @@ -316,7 +316,7 @@ class MyIdMenu: Reactor.Component {
  • {svg_checkmark}{translate('Enable file transfer')}
  • {svg_checkmark}{translate('Enable camera')}
  • {svg_checkmark}{translate('Enable terminal')}
  • -
  • {svg_checkmark}{translate('Enable remote restart')}
  • +
  • {svg_checkmark}{translate('Enable remote restart')}
  • {svg_checkmark}{translate('Enable TCP tunneling')}
  • {is_win ?
  • {svg_checkmark}{translate('Enable blocking user input')}
  • : ""}
  • {svg_checkmark}{translate('Enable LAN discovery')}
  • @@ -334,14 +334,14 @@ class MyIdMenu: Reactor.Component { {false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connect via relay')}
  • } {handler.is_ok_change_id() ?
    : ""} - {username ? + {username ?
  • {translate('Logout')} ({username})
  • :
  • {translate('Login')}
  • } {handler.is_ok_change_id() && key_confirmed && connect_status > 0 ?
  • {translate('Change ID')}
  • : ""}
  • {svg_checkmark}{translate('Dark Theme')}
  • -
  • {svg_checkmark}{translate('Auto update')}
  • +
  • {svg_checkmark}{translate('Auto update')}
  • {translate('About')} {" "}{handler.get_app_name()}
  • ; @@ -473,7 +473,7 @@ class MyIdMenu: Reactor.Component { var old_proxy = socks5[0] || ""; var old_username = socks5[1] || ""; var old_password = socks5[2] || ""; - msgbox("custom-server", "Socks5 Proxy",
    + msgbox("custom-server", "Socks5 Proxy",
    {translate("Server")}:
    {translate("Username")}:
    {translate("Password")}:
    @@ -578,8 +578,6 @@ class App: Reactor.Component
    {!is_win || handler.is_installed() ? "": } - {software_update_url ? : ""} - {is_win && handler.is_installed() && !software_update_url && handler.is_installed_lower_version() ? : ""} {is_can_screen_recording ? "": } {is_can_screen_recording && !handler.is_process_trusted(false) ? : ""} {!service_stopped && is_can_screen_recording && handler.is_process_trusted(false) && handler.is_installed() && !handler.is_installed_daemon(false) ? : ""} @@ -648,7 +646,7 @@ function download(from, to, args..) { case 1: rqp.params = p; break; case 2: rqp.headers = p; break; } - } + } } view.request(rqp); } @@ -694,7 +692,7 @@ class UpdateMe: Reactor.Component { handler.update_me(path); }; var onerror = function(err) { - msgbox("custom-error", "Download Error", "Failed to download"); + msgbox("custom-error", "Download Error", "Failed to download"); }; var onprogress = function(loaded, total) { if (!total) total = 5 * 1024 * 1024; @@ -732,7 +730,7 @@ class TrustMe: Reactor.Component { handler.is_process_trusted(true); watch_trust(); } - + event click $(#help-me) { handler.open_url(translate("doc_mac_permission")); } @@ -752,7 +750,7 @@ class CanScreenRecording: Reactor.Component { handler.is_can_screen_recording(true); watch_screen_recording(); } - + event click $(#help-me) { handler.open_url(translate("doc_mac_permission")); } @@ -900,7 +898,7 @@ class PasswordArea: Reactor.Component { function render() { var me = this; self.timer(1ms, function() { me.toggleMenuState() }); - return + return
    {translate('One-time Password')}
    @@ -958,7 +956,7 @@ class PasswordArea: Reactor.Component { var approve_mode= handler.get_option('approve-mode'); var show_password = approve_mode != 'click'; if(show_password && temporaryPasswordLengthMenu) temporaryPasswordLengthMenu.update({show: true }); - var menu = $(menu#edit-password-context); + var menu = $(menu#edit-password-context); me.popup(menu); } @@ -1057,7 +1055,7 @@ function updatePasswordArea() { } if (update) passwordArea.update(); updatePasswordArea(); - }); + }); } updatePasswordArea(); @@ -1197,14 +1195,14 @@ function checkConnectStatus() { app.update(); } check_if_overlay(); - checkConnectStatus(); - }); -} - -var enter = false; -function self.onMouse(evt) { - switch(evt.type) { - case Event.MOUSE_ENTER: + checkConnectStatus(); + }); +} + +var enter = false; +function self.onMouse(evt) { + switch(evt.type) { + case Event.MOUSE_ENTER: enter = true; check_if_overlay(); break; @@ -1240,9 +1238,9 @@ function set_local_user_info(user) { function login() { var name0 = getUserName(); var pass0 = ''; - msgbox("custom-login", translate('Login'),
    -
    {translate('Username')}:
    -
    {translate('Password')}:
    + msgbox("custom-login", translate('Login'),
    +
    {translate('Username')}:
    +
    {translate('Password')}:
    , "", function(res=null, show_progress) { if (!res) return; show_progress(); @@ -1291,11 +1289,11 @@ function on_2fa_check(last_msg) { const secret = last_msg.secret; const emailHint = last_msg.user.email; - msgbox("custom-2fa-verification-code", translate('Verification code'),
    + msgbox("custom-2fa-verification-code", translate('Verification code'),
    { isEmailCheck &&
    {translate('Email')}:{emailHint}
    } -
    {translate(isEmailCheck ? 'Verification code' : '2FA code')}:
    +
    {translate(isEmailCheck ? 'Verification code' : '2FA code')}:
    { isEmailCheck &&
    {translate('verification_tip')}
    } -
    , "", +
    , "", function(res=null, show_progress) { if (!res) return; show_progress(); @@ -1316,7 +1314,7 @@ function on_2fa_check(last_msg) { secret: secret, deviceInfo: getDeviceInfo() }; - httpRequest(url + "/api/login", #post, loginData, + httpRequest(url + "/api/login", #post, loginData, function(data) { if (data.error) { abLoading = false; @@ -1388,7 +1386,7 @@ function refreshCurrentUser() { } handleAbError(err); }, getHttpHeaders()); -} +} function getHttpHeaders() { return "Authorization: Bearer " + handler.get_local_option("access_token"); From 7f5229bb5d5f4c8cb497e2f3daa05418df471c8d Mon Sep 17 00:00:00 2001 From: Matthias Weiss Date: Tue, 12 Aug 2025 14:53:31 +0200 Subject: [PATCH 492/506] Package metadata --- build.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.py b/build.py index 87c0dbd3432..3eeee677c9e 100755 --- a/build.py +++ b/build.py @@ -292,13 +292,13 @@ def generate_control_file(version): control_file_path = "../res/DEBIAN/control" system2('/bin/rm -rf %s' % control_file_path) - content = """Package: rustdesk + content = """Package: rustdesk-connect Section: net Priority: optional Version: %s Architecture: %s -Maintainer: rustdesk -Homepage: https://rustdesk.com +Maintainer: B1 Systems GmbH +Homepage: https://github.com/b1-systems/rustdesk 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. @@ -360,7 +360,7 @@ def build_flutter_deb(version, features): system2('/bin/rm -rf tmpdeb/') system2('/bin/rm -rf ../res/DEBIAN/control') - os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) + os.rename('rustdesk.deb', '../rustdesk-connect_%s-1_%s.deb' % (version, get_deb_arch())) os.chdir("..") @@ -397,7 +397,7 @@ def build_deb_from_folder(version, binary_folder): system2('/bin/rm -rf tmpdeb/') system2('/bin/rm -rf ../res/DEBIAN/control') - os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) + os.rename('rustdesk.deb', '../rustdesk-connect_%s-1_%s.deb' % (version, get_deb_arch())) os.chdir("..") From 424d510577b0c076db1791d8d8e6def1e7a5c8b3 Mon Sep 17 00:00:00 2001 From: Matthias Weiss Date: Tue, 12 Aug 2025 14:54:54 +0200 Subject: [PATCH 493/506] ensure rustdesk service is not running --- res/DEBIAN/postinst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/res/DEBIAN/postinst b/res/DEBIAN/postinst index dad333ee5b9..51e11eefa3b 100755 --- a/res/DEBIAN/postinst +++ b/res/DEBIAN/postinst @@ -6,7 +6,7 @@ if [ "$1" = configure ]; then INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk - + if [ "systemd" == "$INITSYS" ]; then if [ -e /etc/systemd/system/rustdesk.service ]; then @@ -23,7 +23,9 @@ if [ "$1" = configure ]; then sed -i "s|pkill|/usr/bin/pkill|g" /usr/lib/systemd/system/rustdesk.service fi systemctl daemon-reload - systemctl enable rustdesk - systemctl start rustdesk + + # ensure rustdesk service is NOT running on startup + systemctl disable rustdesk + systemctl stop rustdesk fi fi From 414aa2d612050b72ad979c95e44abb139a12713f Mon Sep 17 00:00:00 2001 From: Matthias Weiss Date: Tue, 12 Aug 2025 15:56:01 +0200 Subject: [PATCH 494/506] fix yaml file --- .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 17ffd4edb58..13fbafb2f6e 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1218,7 +1218,7 @@ jobs: if: false needs: [build-rustdesk-android] name: build rustdesk android universal apk - if: ${{ inputs.upload-artifact }} + # if: ${{ inputs.upload-artifact }} runs-on: ubuntu-24.04 env: reltype: release From 3994f7705cfec38683b7373d01cfb707c987338e Mon Sep 17 00:00:00 2001 From: Matthias Weiss Date: Tue, 12 Aug 2025 16:13:34 +0200 Subject: [PATCH 495/506] Remove connect box --- .../lib/desktop/pages/connection_page.dart | 124 +++++++++--------- src/ui/index.tis | 8 -- 2 files changed, 61 insertions(+), 71 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 6f672a75942..2f244011ee5 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -309,12 +309,6 @@ class _ConnectionPageState extends State Expanded( child: Column( children: [ - Row( - children: [ - Flexible(child: _buildRemoteIDTextField(context)), - ], - ).marginOnly(top: 22), - SizedBox(height: 12), Divider().paddingOnly(right: 12), Expanded(child: PeerTabPage()), ], @@ -536,64 +530,68 @@ class _ConnectionPageState extends State builder: (context, setState) { var offset = Offset(0, 0); return Obx(() => InkWell( - child: _menuOpen.value - ? Transform.rotate( - angle: pi, - child: Icon(IconFont.more, size: 14), + child: _menuOpen.value + ? Transform.rotate( + angle: pi, + child: Icon(IconFont.more, size: 14), + ) + : Icon(IconFont.more, size: 14), + onTapDown: (e) { + offset = e.globalPosition; + }, + onTap: () async { + _menuOpen.value = true; + final x = offset.dx; + final y = offset.dy; + await mod_menu + .showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + items: [ + ( + 'Transfer file', + () => onConnect(isFileTransfer: true) + ), + ( + 'View camera', + () => onConnect(isViewCamera: true) + ), + ( + '${translate('Terminal')} (beta)', + () => onConnect(isTerminal: true) + ), + ] + .map((e) => MenuEntryButton( + childBuilder: (TextStyle? style) => + Text( + translate(e.$1), + style: style, + ), + proc: () => e.$2(), + padding: EdgeInsets.symmetric( + horizontal: + kDesktopMenuPadding.left), + dismissOnClicked: true, + )) + .map((e) => e.build( + context, + const MenuConfig( + commonColor: CustomPopupMenuTheme + .commonColor, + height: + CustomPopupMenuTheme.height, + dividerHeight: + CustomPopupMenuTheme + .dividerHeight))) + .expand((i) => i) + .toList(), + elevation: 8, ) - : Icon(IconFont.more, size: 14), - onTapDown: (e) { - offset = e.globalPosition; - }, - onTap: () async { - _menuOpen.value = true; - final x = offset.dx; - final y = offset.dy; - await mod_menu - .showMenu( - context: context, - position: RelativeRect.fromLTRB(x, y, x, y), - items: [ - ( - 'Transfer file', - () => onConnect(isFileTransfer: true) - ), - ( - 'View camera', - () => onConnect(isViewCamera: true) - ), - ( - '${translate('Terminal')} (beta)', - () => onConnect(isTerminal: true) - ), - ] - .map((e) => MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate(e.$1), - style: style, - ), - proc: () => e.$2(), - padding: EdgeInsets.symmetric( - horizontal: kDesktopMenuPadding.left), - dismissOnClicked: true, - )) - .map((e) => e.build( - context, - const MenuConfig( - commonColor: - CustomPopupMenuTheme.commonColor, - height: CustomPopupMenuTheme.height, - dividerHeight: CustomPopupMenuTheme - .dividerHeight))) - .expand((i) => i) - .toList(), - elevation: 8, - ) - .then((_) { - _menuOpen.value = false; - }); - }, - )); + .then((_) { + _menuOpen.value = false; + }); + }, + )); }, ), ), diff --git a/src/ui/index.tis b/src/ui/index.tis index 605da7b0005..570abe18004 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -587,14 +587,6 @@ class App: Reactor.Component
    -
    -
    {translate('Control Remote Desktop')}
    - -
    - - -
    -
    From 270efdc8e622927daeeed448b4e1928f4ee5f79a Mon Sep 17 00:00:00 2001 From: Matthias Weiss Date: Tue, 12 Aug 2025 16:15:02 +0200 Subject: [PATCH 496/506] change package name and metadata --- build.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.py b/build.py index 3eeee677c9e..208b67359d0 100755 --- a/build.py +++ b/build.py @@ -292,7 +292,7 @@ def generate_control_file(version): control_file_path = "../res/DEBIAN/control" system2('/bin/rm -rf %s' % control_file_path) - content = """Package: rustdesk-connect + content = """Package: rustdesk Section: net Priority: optional Version: %s @@ -360,7 +360,7 @@ def build_flutter_deb(version, features): system2('/bin/rm -rf tmpdeb/') system2('/bin/rm -rf ../res/DEBIAN/control') - os.rename('rustdesk.deb', '../rustdesk-connect_%s-1_%s.deb' % (version, get_deb_arch())) + os.rename('rustdesk.deb', '../rustdesk_%s-1_%s.deb' % (version, get_deb_arch())) os.chdir("..") @@ -397,7 +397,7 @@ def build_deb_from_folder(version, binary_folder): system2('/bin/rm -rf tmpdeb/') system2('/bin/rm -rf ../res/DEBIAN/control') - os.rename('rustdesk.deb', '../rustdesk-connect_%s-1_%s.deb' % (version, get_deb_arch())) + os.rename('rustdesk.deb', '../rustdesk_%s-1_%s.deb' % (version, get_deb_arch())) os.chdir("..") From 59d597de8aa3e6728003090e3daf98067b59fb4d Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 13 Aug 2025 10:08:23 +0800 Subject: [PATCH 497/506] show direct connection for IPv6 via RelayResponse (#12634) Signed-off-by: 21pages --- src/client.rs | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/client.rs b/src/client.rs index 624e9a51737..4c2a3c315b4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -214,14 +214,17 @@ impl Client { } } Ok(x) => { - let direct_failures = interface.get_lch().read().unwrap().direct_failures; - let direct = x.0 .1; - if !interface.is_force_relay() && (direct_failures == 0) != direct { - let n = if direct { 0 } else { 1 }; - log::info!("direct_failures updated to {}", n); - interface.get_lch().write().unwrap().set_direct_failure(n); + // Set x.2 to true only in the connect() function to indicate that direct_failures needs to be updated; everywhere else it should be set to false. + if x.2 { + let direct_failures = interface.get_lch().read().unwrap().direct_failures; + let direct = x.0 .1; + if !interface.is_force_relay() && (direct_failures == 0) != direct { + let n = if direct { 0 } else { 1 }; + log::info!("direct_failures updated to {}", n); + interface.get_lch().write().unwrap().set_direct_failure(n); + } } - Ok(x) + Ok((x.0, x.1)) } } } @@ -242,6 +245,7 @@ impl Client { &'static str, ), (i32, String), + bool, )> { if config::is_incoming_only() { bail!("Incoming only mode"); @@ -258,6 +262,7 @@ impl Client { "TCP", ), (0, "".to_owned()), + false, )); } // Allow connect to {domain}:{port} @@ -271,6 +276,7 @@ impl Client { "TCP", ), (0, "".to_owned()), + false, )); } @@ -352,7 +358,7 @@ impl Client { ); connect_futures.push(fut.boxed()); match select_ok(connect_futures).await { - Ok(conn) => Ok((conn.0 .0, conn.0 .1)), + Ok(conn) => Ok((conn.0 .0, conn.0 .1, conn.0 .2)), Err(e) => Err(e), } } @@ -377,6 +383,7 @@ impl Client { &'static str, ), (i32, String), + bool, )> { let mut start = Instant::now(); let mut socket = connect_tcp(&*rendezvous_server, CONNECT_TIMEOUT).await; @@ -565,7 +572,11 @@ impl Client { log::info!("{:?} used to establish {typ} connection", start.elapsed()); let pk = Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?; - return Ok(((conn, false, pk, kcp, typ), (feedback, rendezvous_server))); + return Ok(( + (conn, typ == "IPv6", pk, kcp, typ), + (feedback, rendezvous_server), + false, + )); } _ => { log::error!("Unexpected protobuf msg received: {:?}", msg_in); @@ -611,6 +622,7 @@ impl Client { ) .await?, (feedback, rendezvous_server), + true, )) } From 160edcc1cdedcd30a917a40dd93ec6d1f1a38b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Wed, 13 Aug 2025 13:25:09 +0900 Subject: [PATCH 498/506] Update ko.rs (#12590) * Update ko.rs * Update ko.rs Update Korean --- 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 a880bb86a7b..2f60302b10e 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -708,6 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "사용자가 관리자인지 확인하는 데 실패했습니다."), ("Supported only in the installed version.", "설치된 버전에서만 지원됩니다."), ("elevation_username_tip", "사용자 이름 또는 도메인\\사용자 이름 입력"), - ("Preparing for installation ...", ""), + ("Preparing for installation ...", "설치 준비 중 ..."), ].iter().cloned().collect(); } From d59f216c26990679ddc42a59ef28d0e85c7f607f Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Wed, 13 Aug 2025 06:25:19 +0200 Subject: [PATCH 499/506] Update nl.rs (#12592) --- 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 706bb341bc2..652a7a8041e 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -708,6 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "Fout bij het controleren of de gebruiker een beheerder is."), ("Supported only in the installed version.", "Alleen ondersteund in de geïnstalleerde versie."), ("elevation_username_tip", "Voer je gebruikersnaam of domeinnaam in"), - ("Preparing for installation ...", ""), + ("Preparing for installation ...", "Voorbereiden voor installatie ..."), ].iter().cloned().collect(); } From 5a75ea723bbd97c3fa55aca241042ffc69b8afb7 Mon Sep 17 00:00:00 2001 From: solokot Date: Wed, 13 Aug 2025 07:25:30 +0300 Subject: [PATCH 500/506] Update ru.rs (#12594) --- 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 eb0de735577..30fd697cb84 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -708,6 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "Невозможно проверить, является ли пользователь администратором."), ("Supported only in the installed version.", "Поддерживается только в установочной версии."), ("elevation_username_tip", "Введите пользователя или домен\\пользователя"), - ("Preparing for installation ...", ""), + ("Preparing for installation ...", "Подготовка к установке..."), ].iter().cloned().collect(); } From dc86db52060c51c08b2f9ae6891b2316e2c4a977 Mon Sep 17 00:00:00 2001 From: Lynilia <89228568+Lynilia@users.noreply.github.com> Date: Wed, 13 Aug 2025 06:25:52 +0200 Subject: [PATCH 501/506] Update fr.rs (#12582) --- 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 6b2b2816dfa..384199cd795 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -708,6 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "Échec de la vérification du statut d’administrateur de l’utilisateur."), ("Supported only in the installed version.", "Uniquement pris en charge dans la version installée."), ("elevation_username_tip", "Saisissez un nom d’utilisateur ou un domaine\\utilisateur"), - ("Preparing for installation ...", ""), + ("Preparing for installation ...", "Préparation de l’installation…"), ].iter().cloned().collect(); } From 4e82766ba4da38d294aceaf4372eb18f3ccac580 Mon Sep 17 00:00:00 2001 From: Mahdi Rahimi <31624047+mahdirahimi1999@users.noreply.github.com> Date: Wed, 13 Aug 2025 07:56:17 +0330 Subject: [PATCH 502/506] Update Arabic translation in ar.rs (#12588) --- src/lang/ar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 28b1e74f7f3..a1a84de5971 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -708,6 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "فشل التحقق مما إذا كان المستخدم لديه صلاحيات المسؤول."), ("Supported only in the installed version.", "مدعوم فقط في النسخة المُثبتة."), ("elevation_username_tip", "يرجى إدخال اسم مستخدم بصلاحيات المسؤول للمتابعة."), - ("Preparing for installation ...", ""), + ("Preparing for installation ...", "جارٍ التحضير للتثبيت...") ].iter().cloned().collect(); } From 6f4b23b40bc376d23a22f32d73eac3c1f685c50b Mon Sep 17 00:00:00 2001 From: Mahdi Rahimi <31624047+mahdirahimi1999@users.noreply.github.com> Date: Wed, 13 Aug 2025 07:56:27 +0330 Subject: [PATCH 503/506] Updated Persian translations in fa.rs (#12589) --- src/lang/fa.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 44d40d1e790..548ad7e0e4c 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -708,6 +708,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "بررسی وضعیت مدیر سیستم برای کاربر ناموفق بود."), ("Supported only in the installed version.", "فقط در نسخه نصب‌شده پشتیبانی می‌شود."), ("elevation_username_tip", "لطفاً نام کاربری مدیریتی را برای ارتقاء دسترسی وارد کنید."), - ("Preparing for installation ...", ""), + ("Preparing for installation ...", "در حال آماده‌سازی برای نصب..."), ].iter().cloned().collect(); } From ab11b25d8b0817909b6e6014fd084b268f2a9888 Mon Sep 17 00:00:00 2001 From: Matthias Weiss Date: Wed, 13 Aug 2025 07:44:54 +0200 Subject: [PATCH 504/506] Delete winget.yml --- .github/workflows/winget.yml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .github/workflows/winget.yml diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml deleted file mode 100644 index 2b1bff105a4..00000000000 --- a/.github/workflows/winget.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Publish to WinGet -on: - release: - types: [released] - workflow_dispatch: -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: vedantmgoyal9/winget-releaser@main - with: - identifier: RustDesk.RustDesk - version: "1.4.1" - release-tag: "1.4.1" - token: ${{ secrets.WINGET_TOKEN }} From fd5711489fcebdefa63aa7382c3238f1b8801089 Mon Sep 17 00:00:00 2001 From: Matthias Weiss Date: Wed, 13 Aug 2025 07:47:34 +0200 Subject: [PATCH 505/506] Update flutter-build.yml --- .github/workflows/flutter-build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index a2d56ceb7f5..cbca2f4a2aa 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -547,7 +547,6 @@ jobs: # files: | # flutter/build/ios/ipa/*.ipa - build-for-macOS: if: false name: ${{ matrix.job.target }} @@ -1811,7 +1810,6 @@ jobs: matrix: job: - { target: x86_64-unknown-linux-gnu, arch: x86_64 } - - { target: aarch64-unknown-linux-gnu, arch: aarch64 } steps: - name: Checkout source code uses: actions/checkout@v4 From e1f4cc8a332e9e0dae29dfbfb534001bbba6b12a Mon Sep 17 00:00:00 2001 From: Matthias Weiss Date: Wed, 13 Aug 2025 07:49:08 +0200 Subject: [PATCH 506/506] disable flatpask nightly --- .github/workflows/flutter-build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index cbca2f4a2aa..6d00589f1d1 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -1855,7 +1855,8 @@ jobs: - build-rustdesk-linux - build-rustdesk-linux-sciter runs-on: ${{ matrix.job.on }} - if: ${{ inputs.upload-artifact }} + if: false + # if: ${{ inputs.upload-artifact }} strategy: fail-fast: false matrix: @@ -1927,7 +1928,7 @@ jobs: flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak build-rustdesk-web: - if: False + if: false name: build-rustdesk-web runs-on: ubuntu-22.04 permissions: