diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 4ccd42081..eb13edf15 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -47,8 +47,8 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-release-flutter.zip - Expand-Archive windows-x64-release-flutter.zip -DestinationPath engine + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain @@ -142,13 +142,42 @@ jobs: job: - { target: x86_64-apple-darwin, - os: macos-10.15, + os: macos-latest, extra-build-args: "", } steps: - name: Checkout source code uses: actions/checkout@v3 + - name: Import the codesign cert + uses: apple-actions/import-codesign-certs@v1 + with: + p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} + p12-password: ${{ secrets.MACOS_P12_PASSWORD }} + keychain: rustdesk + + - name: Check sign and import sign key + run: | + security default-keychain -s rustdesk.keychain + security find-identity -v + + - name: Import notarize key + uses: timheuer/base64-to-file@v1.2 + with: + # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling + fileName: rustdesk.json + fileDir: ${{ github.workspace }} + encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} + + - name: Install rcodesign tool + shell: bash + run: | + pushd /tmp + wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz + mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin + popd + - name: Install build runtime run: | brew install llvm create-dmg nasm yasm cmake gcc wget ninja @@ -158,7 +187,6 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -177,8 +205,12 @@ jobs: run: | dart pub global activate ffigen --version 5.0.1 # flutter_rust_bridge - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd /tmp + wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz + tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz + mkdir -p ~/.cargo/bin + mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen + popd pushd flutter && flutter pub get && popd ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -192,10 +224,6 @@ jobs: run: | $VCPKG_ROOT/vcpkg install libvpx libyuv opus - - name: Install cargo bundle tools - run: | - cargo install cargo-bundle - - name: Show version information (Rust, cargo, Clang) shell: bash run: | @@ -211,6 +239,18 @@ jobs: # --hwcodec not supported on macos yet ./build.py --flutter ${{ matrix.job.extra-build-args }} + - name: Codesign app and create signed dmg + run: | + security default-keychain -s rustdesk.keychain + security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain + # start sign the rustdesk.app and dmg + rm rustdesk-${{ env.VERSION }}.dmg || true + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/rustdesk.app -v + 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 rustdesk-${{ env.VERSION }}.dmg -v + # notarize the rustdesk-${{ env.VERSION }}.dmg + rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg + - name: Rename rustdesk run: | for name in rustdesk*??.dmg; do @@ -377,7 +417,7 @@ jobs: run: | ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - name: Upload Artifcat + - name: Upload Artifact uses: actions/upload-artifact@master with: name: bridge-artifact @@ -559,6 +599,12 @@ jobs: os: ubuntu-20.04, extra-build-features: "flatpak", } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "appimage", + } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - name: Maximize build space @@ -1012,7 +1058,7 @@ jobs: files: | rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - name: Upload Artifcat + - name: Upload Artifact uses: actions/upload-artifact@master if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} with: @@ -1108,6 +1154,12 @@ jobs: os: ubuntu-18.04, extra-build-features: "flatpak", } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "appimage", + } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - name: Checkout source code @@ -1122,7 +1174,7 @@ jobs: - name: Prepare env run: | sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools mkdir -p ./target/release/ - name: Restore the rustdesk lib file @@ -1177,10 +1229,12 @@ jobs: shell: bash run: | for name in rustdesk*??.deb; do - mv "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + # use cp to duplicate deb files to fit other packages. + cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" done - name: Publish debian package + if: ${{ matrix.job.extra-build-features == '' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -1188,7 +1242,7 @@ jobs: files: | rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - name: Upload Artifcat + - name: Upload Artifact uses: actions/upload-artifact@master if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} with: @@ -1244,6 +1298,29 @@ jobs: files: | res/rustdesk*.zst + - name: Build appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + shell: bash + run: | + # 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 + # run appimage-builder + pushd appimage + sudo appimage-builder --skip-tests + + - name: Publish appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage + - name: Publish fedora28/centos8 package if: ${{ matrix.job.extra-build-features == '' }} uses: softprops/action-gh-release@v1 diff --git a/Cargo.lock b/Cargo.lock index 237369d2c..659702704 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,7 +76,7 @@ dependencies = [ "alsa-sys", "bitflags", "libc", - "nix 0.23.1", + "nix 0.23.2", ] [[package]] @@ -175,11 +175,11 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" dependencies = [ - "concurrent-queue 1.2.4", + "concurrent-queue", "event-listener", "futures-core", ] @@ -192,7 +192,7 @@ checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" dependencies = [ "async-lock", "async-task", - "concurrent-queue 2.0.0", + "concurrent-queue", "fastrand", "futures-lite", "slab", @@ -200,13 +200,13 @@ dependencies = [ [[package]] name = "async-io" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" dependencies = [ "async-lock", "autocfg 1.1.0", - "concurrent-queue 1.2.4", + "concurrent-queue", "futures-lite", "libc", "log", @@ -215,7 +215,7 @@ dependencies = [ "slab", "socket2 0.4.7", "waker-fn", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] @@ -230,20 +230,20 @@ dependencies = [ [[package]] name = "async-process" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c" +checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4" dependencies = [ "async-io", + "async-lock", "autocfg 1.1.0", "blocking", "cfg-if 1.0.0", "event-listener", "futures-lite", "libc", - "once_cell", "signal-hook", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] @@ -265,9 +265,9 @@ checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" [[package]] name = "async-trait" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" dependencies = [ "proc-macro2", "quote", @@ -438,16 +438,16 @@ dependencies = [ [[package]] name = "blocking" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" dependencies = [ "async-channel", + "async-lock", "async-task", "atomic-waker", "fastrand", "futures-lite", - "once_cell", ] [[package]] @@ -495,15 +495,9 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] -[[package]] -name = "cache-padded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" - [[package]] name = "cairo-rs" version = "0.15.12" @@ -544,7 +538,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ad0e1e3e88dd237a156ab9f571021b8a158caa0ae44b1968a241efb5144c1e" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -553,7 +547,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -565,7 +559,7 @@ dependencies = [ "camino", "cargo-platform", "semver 1.0.14", - "serde 1.0.147", + "serde 1.0.149", "serde_json 1.0.89", ] @@ -581,7 +575,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "serde 1.0.147", + "serde 1.0.149", "serde_json 1.0.89", "syn", "tempfile", @@ -643,7 +637,7 @@ dependencies = [ "js-sys", "num-integer", "num-traits 0.2.15", - "time 0.1.44", + "time 0.1.45", "wasm-bindgen", "winapi 0.3.9", ] @@ -733,7 +727,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "thiserror", ] @@ -824,15 +818,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "concurrent-queue" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" -dependencies = [ - "cache-padded", -] - [[package]] name = "concurrent-queue" version = "2.0.0" @@ -848,7 +833,7 @@ version = "0.4.0" source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" dependencies = [ "directories-next", - "serde 1.0.147", + "serde 1.0.149", "thiserror", "toml", ] @@ -976,7 +961,7 @@ dependencies = [ "mach", "ndk 0.6.0", "ndk-glue 0.6.2", - "nix 0.23.1", + "nix 0.23.2", "oboe", "parking_lot 0.11.2", "stdweb", @@ -1068,12 +1053,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.2.3" +version = "3.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d91974fbbe88ec1df0c24a4f00f99583667a7e2e6272b2b92d294d81e462173" +checksum = "1631ca6e3c59112501a9d87fd86f21591ff77acd31331e8a73f8d80a65bbdd71" dependencies = [ - "nix 0.25.0", - "winapi 0.3.9", + "nix 0.26.1", + "windows-sys 0.42.0", ] [[package]] @@ -1084,9 +1069,9 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] name = "cxx" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" +checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf" dependencies = [ "cc", "cxxbridge-flags", @@ -1096,9 +1081,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" +checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39" dependencies = [ "cc", "codespan-reporting", @@ -1111,15 +1096,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" +checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12" [[package]] name = "cxxbridge-macro" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" +checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" dependencies = [ "proc-macro2", "quote", @@ -1332,8 +1317,7 @@ checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" [[package]] name = "default-net" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e70d471b0ba4e722c85651b3bb04b6880dfdb1224a43ade80c1295314db646" +source = "git+https://github.com/Kingtous/default-net#bdaad8dd5b08efcba303e71729d3d0b1d5ccdb25" dependencies = [ "libc", "memalloc", @@ -1458,7 +1442,7 @@ checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" dependencies = [ "lazy_static", "regex", - "serde 1.0.147", + "serde 1.0.149", "strsim 0.10.0", ] @@ -1481,7 +1465,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "thiserror", ] @@ -1503,9 +1487,9 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "embed-resource" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc24ff8d764818e9ab17963b0593c535f077a513f565e75e4352d758bc4d8c0" +checksum = "e62abb876c07e4754fae5c14cafa77937841f01740637e17d78dc04352f32a5e" dependencies = [ "cc", "rustc_version 0.4.0", @@ -1533,8 +1517,8 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev 0.5.0-2 (git+https://github.com/asur4s/rdev)", - "serde 1.0.147", + "rdev", + "serde 1.0.149", "serde_derive", "tfc", "unicode-segmentation", @@ -1580,7 +1564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" dependencies = [ "enumflags2_derive", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -1679,7 +1663,7 @@ source = "git+https://github.com/fufesou/evdev#cec616e37790293d2cd2aa54a96601ed6 dependencies = [ "bitvec", "libc", - "nix 0.23.1", + "nix 0.23.2", ] [[package]] @@ -1718,9 +1702,9 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" dependencies = [ "cfg-if 1.0.0", "libc", @@ -1730,12 +1714,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", - "miniz_oxide 0.5.4", + "miniz_oxide 0.6.2", ] [[package]] @@ -1787,7 +1771,7 @@ dependencies = [ "pathdiff", "quote", "regex", - "serde 1.0.147", + "serde 1.0.149", "serde_yaml", "structopt", "syn", @@ -2007,7 +1991,7 @@ version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" dependencies = [ - "gio-sys", + "gio-sys 0.15.10", "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", @@ -2022,7 +2006,7 @@ checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", - "gio-sys", + "gio-sys 0.15.10", "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", @@ -2078,7 +2062,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", - "gio-sys", + "gio-sys 0.15.10", "glib 0.15.12", "libc", "once_cell", @@ -2098,6 +2082,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "gio-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" +dependencies = [ + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "system-deps 6.0.3", + "winapi 0.3.9", +] + [[package]] name = "glib" version = "0.10.3" @@ -2137,6 +2134,28 @@ dependencies = [ "thiserror", ] +[[package]] +name = "glib" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cd04d150a2c63e6779f43aec7e04f5374252479b7bed5f45146d9c0e821f161" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.16.3", + "glib-macros 0.16.3", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + [[package]] name = "glib-macros" version = "0.10.1" @@ -2168,6 +2187,21 @@ dependencies = [ "syn", ] +[[package]] +name = "glib-macros" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" +dependencies = [ + "anyhow", + "heck 0.4.0", + "proc-macro-crate 1.2.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "glib-sys" version = "0.10.1" @@ -2188,6 +2222,16 @@ dependencies = [ "system-deps 6.0.3", ] +[[package]] +name = "glib-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" +dependencies = [ + "libc", + "system-deps 6.0.3", +] + [[package]] name = "glob" version = "0.3.0" @@ -2216,6 +2260,17 @@ dependencies = [ "system-deps 6.0.3", ] +[[package]] +name = "gobject-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" +dependencies = [ + "glib-sys 0.16.3", + "libc", + "system-deps 6.0.3", +] + [[package]] name = "gstreamer" version = "0.16.7" @@ -2382,7 +2437,7 @@ dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", - "gio-sys", + "gio-sys 0.15.10", "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", @@ -2455,7 +2510,7 @@ dependencies = [ "quinn", "rand 0.8.5", "regex", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "serde_json 1.0.89", "socket2 0.3.19", @@ -2547,12 +2602,12 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.1.0" -source = "git+https://github.com/21pages/hwcodec#f54d69b35251ade110373403ddefcb8b49c87305" +source = "git+https://github.com/21pages/hwcodec#e819484c4c010199f2a0977bdf306b4edbeafbae" dependencies = [ "bindgen 0.59.2", "cc", "log", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "serde_json 1.0.89", ] @@ -2583,9 +2638,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59df7c4e19c950e6e0e868dcc0a300b09a9b88e9ec55bd879ca819087a77355d" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" dependencies = [ "http", "hyper", @@ -2730,9 +2785,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" +checksum = "ec947b7a4ce12e3b87e353abae7ce124d025b6c7d6c5aea5cc0bcf92e9510ded" [[package]] name = "itertools" @@ -2847,9 +2902,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.137" +version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" [[package]] name = "libdbus-sys" @@ -2979,7 +3034,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b238e3235c8382b7653c6408ed1b08dd379bdb9fdf990fb0bbae3db2cc0ae963" dependencies = [ - "nix 0.23.1", + "nix 0.23.2", "winapi 0.3.9", ] @@ -3004,7 +3059,7 @@ dependencies = [ [[package]] name = "magnum-opus" version = "0.4.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/magnum-opus#6247071a64af7b18e2d553e235729e6865f63ece" +source = "git+https://github.com/rustdesk/magnum-opus#79be072c939168e907fe851690759dcfd6a326af" dependencies = [ "bindgen 0.59.2", "target_build_utils", @@ -3104,6 +3159,15 @@ dependencies = [ "adler", ] +[[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.6.23" @@ -3180,7 +3244,7 @@ dependencies = [ [[package]] name = "mouce" version = "0.2.1" -source = "git+https://github.com/fufesou/mouce.git#aa18ba25bb47484282e972a4b95a8e1d753230b5" +source = "git+https://github.com/fufesou/mouce.git#ed83800d532b95d70e39915314f6052aa433e9b9" dependencies = [ "glob", ] @@ -3330,9 +3394,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" dependencies = [ "bitflags", "cc", @@ -3343,9 +3407,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.24.2" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -3355,9 +3419,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ "autocfg 1.1.0", "bitflags", @@ -3367,6 +3431,18 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nix" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "libc", + "static_assertions", +] + [[package]] name = "nom" version = "7.1.1" @@ -3592,9 +3668,9 @@ dependencies = [ [[package]] name = "ordered-stream" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034ce384018b245e8d8424bbe90577fbd91a533be74107e465e3474eb2285eef" +checksum = "01ca8c99d73c6e92ac1358f9f692c22c0bfd9c4701fa086f5d365c0d4ea818ea" dependencies = [ "futures-core", "pin-project-lite", @@ -3676,7 +3752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.4", + "parking_lot_core 0.9.5", ] [[package]] @@ -3695,9 +3771,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" dependencies = [ "cfg-if 1.0.0", "libc", @@ -3732,9 +3808,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.4.1" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a528564cc62c19a7acac4d81e01f39e53e25e17b934878f4c6d25cc2836e62f8" +checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0" dependencies = [ "thiserror", "ucd-trie", @@ -3830,16 +3906,16 @@ dependencies = [ [[package]] name = "polling" -version = "2.4.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2" +checksum = "166ca89eb77fd403230b9c156612965a81e094ec6ec3aa13663d4c8b113fa748" dependencies = [ "autocfg 1.1.0", "cfg-if 1.0.0", "libc", "log", "wepoll-ffi", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] @@ -4205,11 +4281,10 @@ dependencies = [ [[package]] name = "rayon" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" dependencies = [ - "crossbeam-deque", "either", "rayon-core", ] @@ -4229,7 +4304,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#4051761e7ccf434a443b8e9592c23160c9cace56" +source = "git+https://github.com/fufesou/rdev#1be26c7e8ed0d43cebdd8331d467bb61130a2e6e" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4240,34 +4315,13 @@ dependencies = [ "inotify", "lazy_static", "libc", + "log", "mio 0.8.5", "strum 0.24.1", "strum_macros 0.24.3", "widestring 1.0.2", "winapi 0.3.9", - "x11 2.20.0", -] - -[[package]] -name = "rdev" -version = "0.5.0-2" -source = "git+https://github.com/rustdesk/rdev#25c29f61bfdf5d8ec50f0a8a7743bc1d85eb2c04" -dependencies = [ - "cocoa", - "core-foundation 0.9.3", - "core-foundation-sys 0.8.3", - "core-graphics 0.22.3", - "enum-map", - "epoll", - "inotify", - "lazy_static", - "libc", - "mio 0.8.5", - "strum 0.24.1", - "strum_macros 0.24.3", - "widestring 1.0.2", - "winapi 0.3.9", - "x11 2.20.0", + "x11 2.20.1", ] [[package]] @@ -4281,9 +4335,9 @@ dependencies = [ [[package]] name = "realfft" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3052e66d6ebeff8049607775c41d39a58d1dfa91a2733e89f2b7816bce2ea4cc" +checksum = "93d6b8e8f0c6d2234aa58048d7290c60bf92cd36fd2888cd8331c66ad4f2e1d2" dependencies = [ "rustfft", ] @@ -4369,7 +4423,7 @@ dependencies = [ "pin-project-lite", "rustls", "rustls-pemfile 1.0.1", - "serde 1.0.147", + "serde 1.0.149", "serde_json 1.0.89", "serde_urlencoded", "tokio", @@ -4411,9 +4465,20 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.1.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20c9f5d2a0c3e2ea729ab3706d22217177770654c3ef5056b68b69d07332d3f5" +checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +dependencies = [ + "libc", + "rtoolbox", + "winapi 0.3.9", +] + +[[package]] +name = "rtoolbox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" dependencies = [ "libc", "winapi 0.3.9", @@ -4497,6 +4562,7 @@ dependencies = [ "arboard", "async-process", "async-trait", + "backtrace", "base64", "bytes", "cc", @@ -4522,6 +4588,7 @@ dependencies = [ "flexi_logger", "flutter_rust_bridge", "flutter_rust_bridge_codegen", + "glib 0.16.5", "gtk", "hbb_common", "hound", @@ -4540,17 +4607,17 @@ dependencies = [ "num_cpus", "objc", "parity-tokio-ipc", - "rdev 0.5.0-2 (git+https://github.com/rustdesk/rdev)", + "rdev", "repng", "reqwest", - "rpassword 7.1.0", + "rpassword 7.2.0", "rubato", "runas", "rust-pulsectl", "samplerate", "sciter-rs", "scrap", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "serde_json 1.0.89", "sha2", @@ -4727,7 +4794,7 @@ dependencies = [ "num_cpus", "quest", "repng", - "serde 1.0.147", + "serde 1.0.149", "serde_json 1.0.89", "target_build_utils", "tracing", @@ -4789,7 +4856,7 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -4809,18 +4876,18 @@ checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" [[package]] name = "serde" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" dependencies = [ "proc-macro2", "quote", @@ -4847,7 +4914,7 @@ checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa 1.0.4", "ryu", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -4870,7 +4937,7 @@ dependencies = [ "form_urlencoded", "itoa 1.0.4", "ryu", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -4881,7 +4948,7 @@ checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", - "serde 1.0.147", + "serde 1.0.149", "yaml-rust", ] @@ -4915,7 +4982,7 @@ checksum = "ba8593196da75d9dc4f69349682bd4c2099f8cde114257d1ef7ef1b33d1aba54" dependencies = [ "cfg-if 1.0.0", "libc", - "nix 0.23.1", + "nix 0.23.2", "rand 0.8.5", "win-sys", ] @@ -4963,7 +5030,7 @@ version = "0.1.0" dependencies = [ "confy", "hbb_common", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "walkdir", ] @@ -5038,7 +5105,7 @@ dependencies = [ "ed25519", "libc", "libsodium-sys", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -5146,9 +5213,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ "proc-macro2", "quote", @@ -5320,7 +5387,7 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/asur4s/The-Fat-Controller#48303c5dacded6ea1873bc5d69bdde3175cf336a" +source = "git+https://github.com/fufesou/The-Fat-Controller#48303c5dacded6ea1873bc5d69bdde3175cf336a" dependencies = [ "core-graphics 0.22.3", "unicode-segmentation", @@ -5370,9 +5437,9 @@ dependencies = [ [[package]] name = "time" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", @@ -5414,9 +5481,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" dependencies = [ "autocfg 1.1.0", "bytes", @@ -5429,14 +5496,14 @@ dependencies = [ "signal-hook-registry", "socket2 0.4.7", "tokio-macros", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", @@ -5494,7 +5561,7 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -5580,9 +5647,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "ucd-trie" @@ -5654,7 +5721,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -5711,9 +5778,9 @@ dependencies = [ [[package]] name = "vswhom-sys" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" dependencies = [ "cc", "libc", @@ -5833,7 +5900,7 @@ dependencies = [ "bitflags", "downcast-rs", "libc", - "nix 0.24.2", + "nix 0.24.3", "scoped-tls", "wayland-commons", "wayland-scanner", @@ -5846,7 +5913,7 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" dependencies = [ - "nix 0.24.2", + "nix 0.24.3", "once_cell", "smallvec", "wayland-sys", @@ -5858,7 +5925,7 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" dependencies = [ - "nix 0.24.2", + "nix 0.24.3", "wayland-client", "xcursor", ] @@ -5937,9 +6004,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ "webpki", ] @@ -6404,9 +6471,9 @@ dependencies = [ [[package]] name = "x11" -version = "2.20.0" +version = "2.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ae97874a928d821b061fce3d1fc52f08071dd53c89a6102bc06efcac3b2908" +checksum = "c2638d5b9c17ac40575fb54bb461a4b1d2a8d1b4ffcc4ff237d254ec59ddeb82" dependencies = [ "libc", "pkg-config", @@ -6414,9 +6481,9 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.20.0" +version = "2.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c83627bc137605acc00bb399c7b908ef460b621fc37c953db2b09f88c449ea6" +checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0" dependencies = [ "lazy_static", "libc", @@ -6461,9 +6528,9 @@ dependencies = [ [[package]] name = "zbus" -version = "3.5.0" +version = "3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a25ae891bd547674b368906552115143031c16c23a0f2f4b2f5f5436ab2e6a9f" +checksum = "938ea6da98c75c2c37a86007bd17fd8e208cbec24e086108c87ece98e9edec0d" dependencies = [ "async-broadcast", "async-channel", @@ -6482,11 +6549,11 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix 0.25.0", + "nix 0.25.1", "once_cell", "ordered-stream", "rand 0.8.5", - "serde 1.0.147", + "serde 1.0.149", "serde_repr", "sha1", "static_assertions", @@ -6500,9 +6567,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.5.0" +version = "3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aa37701ce7b3a43632d2b0ad9d4aef602b46be6bdd7fba3b7c5007f9f6eb2c2" +checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e" dependencies = [ "proc-macro-crate 1.2.1", "proc-macro2", @@ -6513,11 +6580,11 @@ dependencies = [ [[package]] name = "zbus_names" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69bb79b44e1901ed8b217e485d0f01991aec574479b68cb03415f142bc7ae67" +checksum = "6c737644108627748a660d038974160e0cbb62605536091bdfa28fd7f64d43c8" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", "static_assertions", "zvariant", ] @@ -6553,23 +6620,23 @@ dependencies = [ [[package]] name = "zvariant" -version = "3.8.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c817f416f05fcbc833902f1e6064b72b1778573978cfeac54731451ccc9e207" +checksum = "56f8c89c183461e11867ded456db252eae90874bc6769b7adbea464caa777e51" dependencies = [ "byteorder", "enumflags2", "libc", - "serde 1.0.147", + "serde 1.0.149", "static_assertions", "zvariant_derive", ] [[package]] name = "zvariant_derive" -version = "3.8.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd24fffd02794a76eb10109de463444064c88f5adb9e9d1a78488adc332bfef" +checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" dependencies = [ "proc-macro-crate 1.2.1", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index a783b1abe..2713df11d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ libc = "0.2" parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" } flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] } runas = "0.2" -magnum-opus = { git = "https://github.com/SoLongAndThanksForAllThePizza/magnum-opus" } +magnum-opus = { git = "https://github.com/rustdesk/magnum-opus" } dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true } rubato = { version = "0.12", optional = true } samplerate = { version = "0.2", optional = true } @@ -59,11 +59,11 @@ base64 = "0.13" sysinfo = "0.24" num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } -default-net = "0.11.0" +default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } errno = "0.2.8" -rdev = { git = "https://github.com/rustdesk/rdev" } +rdev = { git = "https://github.com/fufesou/rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } @@ -118,6 +118,8 @@ dbus = "0.9" dbus-crossroads = "0.5" gtk = "0.15" libappindicator = "0.7" +glib = "0.16.5" +backtrace = "0.3" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" diff --git a/README.md b/README.md index ad19edaa1..79255e455 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Below are the servers you are using for free, it may change along the time. If y | Germany | Codext | 4 vCPU / 8GB RAM | | Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | | USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| Ukraine (Kyiv) | dc.volia (2VM) | 2 vCPU / 4GB RAM | ## Dependencies diff --git a/appimage/AppImageBuilder.yml b/appimage/AppImageBuilder.yml new file mode 100644 index 000000000..ae95fd2ce --- /dev/null +++ b/appimage/AppImageBuilder.yml @@ -0,0 +1,87 @@ +# appimage-builder recipe see https://appimage-builder.readthedocs.io for details +version: 1 +script: + - rm -rf ./AppDir || true + - bsdtar -zxvf ../rustdesk-1.2.0.deb + - tar -xvf ./data.tar.xz + - mkdir ./AppDir + - mv ./usr ./AppDir/usr + # 32x32 icon + - for i in {32,64,128}; do mkdir -p ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/; cp ../res/$i\x$i.png ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/rustdesk.png; done + # desktop file + # - sed -i "s/Icon=\/usr\/share\/rustdesk\/files\/rustdesk.png/Icon=rustdesk/g" ./AppDir/usr/share/applications/rustdesk.desktop + - rm -rf ./AppDir/usr/share/applications +AppDir: + path: ./AppDir + app_info: + id: rustdesk + name: rustdesk + icon: rustdesk + version: 1.2.0 + exec: usr/lib/rustdesk/rustdesk + exec_args: $@ + apt: + arch: + - amd64 + allow_unauthenticated: true + sources: + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted + universe multiverse + - sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu + bionic main + include: + - libc6:amd64 + - libgtk-3-0 + - libxcb-randr0 + - libxdo3 + - libxfixes3 + - libxcb-shape0 + - libxcb-xfixes0 + - libasound2 + - libsystemd0 + - curl + - libva-drm2 + - libva-x11-2 + - libvdpau1 + - libgstreamer-plugins-base1.0-0 + exclude: + - humanity-icon-theme + - hicolor-icon-theme + - adwaita-icon-theme + - ubuntu-mono + files: + include: [] + exclude: + - usr/share/man + - usr/share/doc/*/README.* + - usr/share/doc/*/changelog.* + - usr/share/doc/*/NEWS.* + - usr/share/doc/*/TODO.* + runtime: + env: + GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/ + test: + fedora-30: + image: appimagecrafters/tests-env:fedora-30 + command: ./AppRun + debian-stable: + image: appimagecrafters/tests-env:debian-stable + command: ./AppRun + archlinux-latest: + image: appimagecrafters/tests-env:archlinux-latest + command: ./AppRun + centos-7: + image: appimagecrafters/tests-env:centos-7 + command: ./AppRun + ubuntu-xenial: + image: appimagecrafters/tests-env:ubuntu-xenial + command: ./AppRun +AppImage: + arch: x86_64 + update-information: guess diff --git a/build.py b/build.py index 127469784..6b107ff4b 100755 --- a/build.py +++ b/build.py @@ -21,7 +21,7 @@ skip_cargo = False def custom_os_system(cmd): err = os._system(cmd) if err != 0: - print(f"Error occured when executing: {cmd}. Exiting.") + print(f"Error occurred when executing: {cmd}. Exiting.") sys.exit(-1) # replace prebuilt os.system os._system = os.system @@ -99,6 +99,11 @@ def make_parser(): action='store_true', help='Build rustdesk libs with the flatpak feature enabled' ) + parser.add_argument( + '--appimage', + action='store_true', + help='Build rustdesk libs with the appimage feature enabled' + ) parser.add_argument( '--skip-cargo', action='store_true', @@ -236,6 +241,8 @@ def get_features(args): features.append('flutter') if args.flatpak: features.append('flatpak') + if args.appimage: + features.append('appimage') print("features:", features) return features @@ -305,7 +312,8 @@ def build_flutter_deb(version, features): def build_flutter_dmg(version, features): if not skip_cargo: - os.system(f'cargo build --features {features} --lib --release') + # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project + os.system(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') # copy dylib os.system( "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") @@ -469,6 +477,7 @@ def main(): if pa: os.system(''' # buggy: rcodesign sign ... path/*, have to sign one by one + # install rcodesign via cargo install apple-codesign #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/libsciter.dylib #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app @@ -481,9 +490,15 @@ def main(): version, 'rustdesk-%s.dmg' % version) if pa: os.system(''' + # https://pyoxidizer.readthedocs.io/en/apple-codesign-0.14.0/apple_codesign.html + # https://pyoxidizer.readthedocs.io/en/stable/tugger_code_signing.html + # https://developer.apple.com/developer-id/ + # goto xcode and login with apple id, manager certificates (Developer ID Application and/or Developer ID Installer) online there (only download and double click (install) cer file can not export p12 because no private key) #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg - # https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html + # https://appstoreconnect.apple.com/access/api + # https://gregoryszorc.com/docs/apple-codesign/0.16.0/apple_codesign_rcodesign.html#notarizing-and-stapling + # p8 file is generated when you generate api key, download and put it under ~/.private_keys/ rcodesign notarize --api-issuer {2} --api-key {3} --staple ./rustdesk-{1}.dmg # verify: spctl -a -t exec -v /Applications/RustDesk.app '''.format(pa, version, os.environ.get('api-issuer'), os.environ.get('api-key'))) diff --git a/build.rs b/build.rs index 67e40752c..ade63f0bc 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,16 @@ #[cfg(windows)] fn build_windows() { - cc::Build::new().file("src/windows.cc").compile("windows"); + let file = "src/platform/windows.cc"; + cc::Build::new().file(file).compile("windows"); println!("cargo:rustc-link-lib=WtsApi32"); - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=windows.cc"); + println!("cargo:rerun-if-changed={}", file); +} + +#[cfg(target_os = "macos")] +fn build_mac() { + let file = "src/platform/macos.mm"; + cc::Build::new().file(file).compile("macos"); + println!("cargo:rerun-if-changed={}", file); } #[cfg(all(windows, feature = "inline"))] @@ -117,5 +124,8 @@ fn main() { #[cfg(windows)] build_windows(); #[cfg(target_os = "macos")] + build_mac(); + #[cfg(target_os = "macos")] println!("cargo:rustc-link-lib=framework=ApplicationServices"); + println!("cargo:rerun-if-changed=build.rs"); } diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 966ad3df8..1f35ef92d 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -4,7 +4,7 @@ Doc: https://rustdesk.com/docs/en/manual/mobile/ 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, RustDesk uses AccessibilityService API to implement Addroid remote control. -In addtion to remote control, you can also transfer files between Android devices and PCs easily with RustDesk. +In addition to remote control, you can also transfer files between Android devices and PCs easily with RustDesk. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, or self-hosting, or write your own rendezvous/relay server. Self-hosting server is free and open source: https://github.com/rustdesk/rustdesk-server diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png index 543fe8346..3668c7106 100644 Binary files a/fastlane/metadata/android/en-US/images/icon.png and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index 32e7b3554..e84ed4d21 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png index 0f9368545..5a83dc1f0 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png index b59279552..629631ac7 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png index a4048ae69..39a15ba77 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png index 5d726ab36..5574ee7dc 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png index 2c3fad113..8e0a83a6a 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png index 5873757f9..0618ae0b6 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png index faea2eb16..560902b03 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png differ diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt new file mode 100644 index 000000000..effb820d6 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -0,0 +1,11 @@ +Une application de bureau à distance open source, l'alternative open source à TeamViewer. +Code source : https://github.com/rustdesk/rustdesk +Doc : https://rustdesk.com/docs/en/manual/mobile/ + +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é", RustDesk utilise l'API AccessibilityService pour implémenter la télécommande Addroid. + +En plus du contrôle à distance, vous pouvez également transférer facilement des fichiers entre des appareils Android et des PC avec RustDesk. + +Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, ou l'auto-hébergement, ou écrire votre propre serveur de rendez-vous/relais. Le serveur auto-hébergé est gratuit et open source : https://github.com/rustdesk/rustdesk-server + +Veuillez télécharger et installer la version de bureau à partir de : https://rustdesk.com, vous pourrez alors accéder et contrôler votre bureau à partir de votre mobile, ou contrôler votre mobile à partir du bureau. diff --git a/fastlane/metadata/android/fr-FR/short_description.txt b/fastlane/metadata/android/fr-FR/short_description.txt new file mode 100644 index 000000000..e1f4b4b0f --- /dev/null +++ b/fastlane/metadata/android/fr-FR/short_description.txt @@ -0,0 +1 @@ +Une application de bureau à distance open source, l'alternative open source à TeamViewer. diff --git a/flutter/.gitignore b/flutter/.gitignore index 3cbfc0f54..9c7e52c12 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -54,3 +54,4 @@ lib/generated_bridge.freezed.dart flutter_export_environment.sh Flutter-Generated.xcconfig key.jks +macos/rustdesk.xcodeproj/project.xcworkspace/ diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index b3dc255d5..d5d2c49c8 100644 Binary files a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index f24291dbf..e30cc5019 100644 Binary files a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 8a4e80f53..41ccba607 100644 Binary files a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 8bb8d570f..c10349d71 100644 Binary files a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 2130526a6..52fde7830 100644 Binary files a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/flutter/assets/kb_layout_iso.svg b/flutter/assets/kb_layout_iso.svg new file mode 100644 index 000000000..69f0c96cb --- /dev/null +++ b/flutter/assets/kb_layout_iso.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/kb_layout_not_iso.svg b/flutter/assets/kb_layout_not_iso.svg new file mode 100644 index 000000000..09a055be3 --- /dev/null +++ b/flutter/assets/kb_layout_not_iso.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/logo.png b/flutter/assets/logo.png index f854a1dc3..ede0e00c4 100644 Binary files a/flutter/assets/logo.png and b/flutter/assets/logo.png differ diff --git a/flutter/build_android_deps.sh b/flutter/build_android_deps.sh index f120346cf..a30abd154 100755 --- a/flutter/build_android_deps.sh +++ b/flutter/build_android_deps.sh @@ -1,7 +1,7 @@ #!/bin/bash -# Build libyuv / opus / libvpx / oboe for Android -# Required: +# Build libyuv / opus / libvpx / oboe for Android +# Required: # 1. set VCPKG_ROOT / ANDROID_NDK path environment variables # 2. vcpkg initialized # 3. ndk, version: 22 (if ndk < 22 you need to change LD as `export LD=$TOOLCHAIN/bin/$NDK_LLVM_TARGET-ld`) @@ -23,7 +23,7 @@ HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/l TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG function build { - ANDROID_ABI=$1 + ANDROID_ABI=$1 VCPKG_TARGET=$2 NDK_LLVM_TARGET=$3 LIBVPX_TARGET=$4 @@ -111,15 +111,15 @@ patch -N -d build/oboe -p1 < ../src/oboe.patch # x86_64-linux-android # i686-linux-android -# LIBVPX_TARGET : -# arm64-android-gcc -# armv7-android-gcc +# LIBVPX_TARGET : +# arm64-android-gcc +# armv7-android-gcc # x86_64-android-gcc -# x86-android-gcc +# x86-android-gcc # args: ANDROID_ABI VCPKG_TARGET NDK_LLVM_TARGET LIBVPX_TARGET build arm64-v8a arm64-android aarch64-linux-android arm64-android-gcc -build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc +build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc # rm -rf build/libvpx # rm -rf build/oboe \ No newline at end of file diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index d4ae9af18..c35862a8c 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 61c13f60b..900bd13fa 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index ed67c27ad..5fc34ce9a 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index f974850af..ab315a4c6 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index ff419814d..6d69c01e1 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index b3cdc7920..b6c8034cd 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 15b2e086d..cf6c7c775 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index ed67c27ad..5fc34ce9a 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 94665b74e..6928a4e6d 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 3cf8a0dc2..a13129e15 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 3cf8a0dc2..a13129e15 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 6e21d8ee9..319e70f91 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 01047f4be..229bdf563 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index ceb721793..caffb26a3 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index e416e5517..751104548 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0f5502f54..ed78a8e09 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -46,7 +46,7 @@ var isWebDesktop = false; var version = ""; int androidVersion = 0; -/// only avaliable for Windows target +/// only available for Windows target int windowsBuildNumber = 0; DesktopType? desktopType; @@ -99,22 +99,28 @@ class IconFont { class ColorThemeExtension extends ThemeExtension { const ColorThemeExtension({ required this.border, + required this.highlight, }); final Color? border; + final Color? highlight; static const light = ColorThemeExtension( border: Color(0xFFCCCCCC), + highlight: Color(0xFFE5E5E5), ); static const dark = ColorThemeExtension( border: Color(0xFF555555), + highlight: Color(0xFF3F3F3F), ); @override - ThemeExtension copyWith({Color? border}) { + ThemeExtension copyWith( + {Color? border, Color? highlight}) { return ColorThemeExtension( border: border ?? this.border, + highlight: highlight ?? this.highlight, ); } @@ -126,6 +132,7 @@ class ColorThemeExtension extends ThemeExtension { } return ColorThemeExtension( border: Color.lerp(border, other.border, t), + highlight: Color.lerp(highlight, other.highlight, t), ); } } @@ -223,7 +230,7 @@ class MyTheme { bind.mainSetLocalOption( key: kCommConfKeyTheme, value: mode.toShortString()); } - bind.mainChangeTheme(dark: currentThemeMode().toShortString()); + bind.mainChangeTheme(dark: mode.toShortString()); } } @@ -1360,13 +1367,13 @@ connect(BuildContext context, String id, } } -Future> getHttpHeaders() async { +Map getHttpHeaders() { return { 'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}' }; } -// Simple wrapper of built-in types for refrence use. +// Simple wrapper of built-in types for reference use. class SimpleWrapper { T value; SimpleWrapper(this.value); @@ -1402,7 +1409,7 @@ Future reloadAllWindows() async { /// Indicate the flutter app is running in portable mode. /// /// [Note] -/// Portable build is only avaliable on Windows. +/// Portable build is only available on Windows. bool isRunningInPortableMode() { if (!Platform.isWindows) { return false; @@ -1411,7 +1418,7 @@ bool isRunningInPortableMode() { } /// Window status callback -void onActiveWindowChanged() async { +Future onActiveWindowChanged() async { print( "[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}"); if (rustDeskWinManager.getActiveWindows().isEmpty) { @@ -1503,3 +1510,53 @@ Pointer getOSVERSIONINFOEXPointer() { bool get kUseCompatibleUiMode => Platform.isWindows && const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion); + +class ServerConfig { + late String idServer; + late String relayServer; + late String apiServer; + late String key; + + ServerConfig( + {String? idServer, String? relayServer, String? apiServer, String? key}) { + this.idServer = idServer?.trim() ?? ''; + this.relayServer = relayServer?.trim() ?? ''; + this.apiServer = apiServer?.trim() ?? ''; + this.key = key?.trim() ?? ''; + } + + /// decode from shared string (from user shared or rustdesk-server generated) + /// also see [encode] + /// throw when decoding failure + ServerConfig.decode(String msg) { + final input = msg.split('').reversed.join(''); + final bytes = base64Decode(base64.normalize(input)); + final json = jsonDecode(utf8.decode(bytes)); + + idServer = json['host'] ?? ''; + relayServer = json['relay'] ?? ''; + apiServer = json['api'] ?? ''; + key = json['key'] ?? ''; + } + + /// encode to shared string + /// also see [ServerConfig.decode] + String encode() { + Map config = {}; + config['host'] = idServer.trim(); + config['relay'] = relayServer.trim(); + config['api'] = apiServer.trim(); + config['key'] = key.trim(); + return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits)) + .split('') + .reversed + .join(); + } + + /// from local options + ServerConfig.fromOptions(Map options) + : idServer = options['custom-rendezvous-server'] ?? "", + relayServer = options['relay-server'] ?? "", + apiServer = options['api-server'] ?? "", + key = options['key'] ?? ""; +} diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart new file mode 100644 index 000000000..27238db67 --- /dev/null +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -0,0 +1,119 @@ +import 'package:flutter_hbb/models/peer_model.dart'; + +class HttpType { + static const kAuthReqTypeAccount = "account"; + static const kAuthReqTypeMobile = "mobile"; + static const kAuthReqTypeSMSCode = "sms_code"; + static const kAuthReqTypeEmailCode = "email_code"; + + static const kAuthResTypeToken = "access_token"; + static const kAuthResTypeEmailCheck = "email_check"; +} + +class UserPayload { + String name = ''; + String email = ''; + String note = ''; + int? status; + String grp = ''; + bool isAdmin = false; + + UserPayload.fromJson(Map json) + : name = json['name'] ?? '', + email = json['email'] ?? '', + note = json['note'] ?? '', + status = json['status'], + grp = json['grp'] ?? '', + isAdmin = json['is_admin'] == true; +} + +class PeerPayload { + String id = ''; + String info = ''; + int? status; + String user = ''; + String user_name = ''; + String note = ''; + + PeerPayload.fromJson(Map json) + : id = json['id'] ?? '', + info = json['info'] ?? '', + status = json['status'], + user = json['user'] ?? '', + user_name = json['user_name'] ?? '', + note = json['note'] ?? ''; + + static Peer toPeer(PeerPayload p) { + return Peer.fromJson({"id": p.id}); + } +} + +class LoginRequest { + String? username; + String? password; + String? id; + String? uuid; + bool? autoLogin; + String? type; + String? verificationCode; + String? deviceInfo; + + LoginRequest( + {this.username, + this.password, + this.id, + this.uuid, + this.autoLogin, + this.type, + this.verificationCode, + this.deviceInfo}); + + LoginRequest.fromJson(Map json) { + username = json['username']; + password = json['password']; + id = json['id']; + uuid = json['uuid']; + autoLogin = json['autoLogin']; + type = json['type']; + verificationCode = json['verificationCode']; + deviceInfo = json['deviceInfo']; + } + + Map toJson() { + final Map data = {}; + data['username'] = username ?? ''; + data['password'] = password ?? ''; + data['id'] = id ?? ''; + data['uuid'] = uuid ?? ''; + data['autoLogin'] = autoLogin ?? ''; + data['type'] = type ?? ''; + data['verificationCode'] = verificationCode ?? ''; + data['deviceInfo'] = deviceInfo ?? ''; + return data; + } +} + +class LoginResponse { + String? access_token; + String? type; + UserPayload? user; + + LoginResponse({this.access_token, this.type, this.user}); + + LoginResponse.fromJson(Map json) { + access_token = json['access_token']; + type = json['type']; + user = json['user'] != null ? UserPayload.fromJson(json['user']) : null; + } +} + +class RequestException implements Exception { + int statusCode; + String cause; + RequestException(this.statusCode, this.cause); + + @override + String toString() { + return "RequestException, statusCode: $statusCode, error: $cause"; + } +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 799b0be67..34d5af485 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -3,14 +3,12 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; -import 'package:flutter_hbb/desktop/widgets/login.dart'; import '../../consts.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import 'package:get/get.dart'; import '../../common.dart'; -import '../../desktop/pages/desktop_home_page.dart'; -import '../../mobile/pages/settings_page.dart'; +import 'login.dart'; class AddressBook extends StatefulWidget { final EdgeInsets? menuPadding; @@ -28,7 +26,6 @@ class _AddressBookState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.pullAb()); } @override @@ -42,25 +39,12 @@ class _AddressBookState extends State { } }); - handleLogin() { - // TODO refactor login dialog for desktop and mobile - if (isDesktop) { - loginDialog().then((success) { - if (success) { - gFFI.abModel.pullAb(); - } - }); - } else { - showLogin(gFFI.dialogManager); - } - } - Future buildBody(BuildContext context) async { return Obx(() { if (gFFI.userModel.userName.value.isEmpty) { return Center( child: InkWell( - onTap: handleLogin, + onTap: loginDialog, child: Text( translate("Login"), style: const TextStyle(decoration: TextDecoration.underline), diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart new file mode 100644 index 000000000..ce27ceb2c --- /dev/null +++ b/flutter/lib/common/widgets/login.dart @@ -0,0 +1,676 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../common.dart'; + +class _IconOP extends StatelessWidget { + final String icon; + final double iconWidth; + final EdgeInsets margin; + const _IconOP( + {Key? key, + required this.icon, + required this.iconWidth, + this.margin = const EdgeInsets.symmetric(horizontal: 4.0)}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: margin, + child: SvgPicture.asset( + 'assets/$icon.svg', + width: iconWidth, + ), + ); + } +} + +class ButtonOP extends StatelessWidget { + final String op; + final RxString curOP; + final double iconWidth; + final Color primaryColor; + final double height; + final Function() onTap; + + const ButtonOP({ + Key? key, + required this.op, + required this.curOP, + required this.iconWidth, + required this.primaryColor, + required this.height, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row(children: [ + Container( + height: height, + width: 200, + child: Obx(() => ElevatedButton( + style: ElevatedButton.styleFrom( + primary: curOP.value.isEmpty || curOP.value == op + ? primaryColor + : Colors.grey, + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), + onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null, + child: Row( + children: [ + SizedBox( + width: 30, + child: _IconOP( + icon: op, + iconWidth: iconWidth, + margin: EdgeInsets.only(right: 5), + )), + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Center( + child: Text('${translate("Continue with")} $op')))), + ], + ))), + ), + ]); + } +} + +class ConfigOP { + final String op; + final double iconWidth; + ConfigOP({required this.op, required this.iconWidth}); +} + +class WidgetOP extends StatefulWidget { + final ConfigOP config; + final RxString curOP; + final Function(String) cbLogin; + const WidgetOP({ + Key? key, + required this.config, + required this.curOP, + required this.cbLogin, + }) : super(key: key); + + @override + State createState() { + return _WidgetOPState(); + } +} + +class _WidgetOPState extends State { + Timer? _updateTimer; + String _stateMsg = ''; + String _failedMsg = ''; + String _url = ''; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + _updateTimer?.cancel(); + } + + _beginQueryState() { + _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { + _updateState(); + }); + } + + _updateState() { + bind.mainAccountAuthResult().then((result) { + if (result.isEmpty) { + return; + } + final resultMap = jsonDecode(result); + if (resultMap == null) { + return; + } + final String stateMsg = resultMap['state_msg']; + String failedMsg = resultMap['failed_msg']; + final String? url = resultMap['url']; + final authBody = resultMap['auth_body']; + if (_stateMsg != stateMsg || _failedMsg != failedMsg) { + if (_url.isEmpty && url != null && url.isNotEmpty) { + launchUrl(Uri.parse(url)); + _url = url; + } + if (authBody != null) { + _updateTimer?.cancel(); + final String username = authBody['user']['name']; + widget.curOP.value = ''; + widget.cbLogin(username); + } + + setState(() { + _stateMsg = stateMsg; + _failedMsg = failedMsg; + if (failedMsg.isNotEmpty) { + widget.curOP.value = ''; + _updateTimer?.cancel(); + } + }); + } + }); + } + + _resetState() { + _stateMsg = ''; + _failedMsg = ''; + _url = ''; + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ButtonOP( + op: widget.config.op, + curOP: widget.curOP, + iconWidth: widget.config.iconWidth, + primaryColor: str2color(widget.config.op, 0x7f), + height: 36, + onTap: () async { + _resetState(); + widget.curOP.value = widget.config.op; + await bind.mainAccountAuth(op: widget.config.op); + _beginQueryState(); + }, + ), + Obx(() { + if (widget.curOP.isNotEmpty && + widget.curOP.value != widget.config.op) { + _failedMsg = ''; + } + return Offstage( + offstage: + _failedMsg.isEmpty && widget.curOP.value != widget.config.op, + child: Row( + children: [ + Text( + _stateMsg, + style: TextStyle(fontSize: 12), + ), + SizedBox(width: 8), + Text( + _failedMsg, + style: TextStyle( + fontSize: 14, + color: Colors.red, + ), + ), + ], + )); + }), + Obx( + () => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: const SizedBox( + height: 5.0, + ), + ), + ), + Obx( + () => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: 20), + child: ElevatedButton( + onPressed: () { + widget.curOP.value = ''; + _updateTimer?.cancel(); + _resetState(); + bind.mainAccountAuthCancel(); + }, + child: Text( + translate('Cancel'), + style: TextStyle(fontSize: 15), + ), + ), + ), + ), + ), + ], + ); + } +} + +class LoginWidgetOP extends StatelessWidget { + final List ops; + final RxString curOP; + final Function(String) cbLogin; + + LoginWidgetOP({ + Key? key, + required this.ops, + required this.curOP, + required this.cbLogin, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var children = ops + .map((op) => [ + WidgetOP( + config: op, + curOP: curOP, + cbLogin: cbLogin, + ), + const Divider( + indent: 5, + endIndent: 5, + ) + ]) + .expand((i) => i) + .toList(); + if (children.isNotEmpty) { + children.removeLast(); + } + return SingleChildScrollView( + child: Container( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: children, + ))); + } +} + +class LoginWidgetUserPass extends StatelessWidget { + final TextEditingController username; + final TextEditingController pass; + final String? usernameMsg; + final String? passMsg; + final bool isInProgress; + final RxString curOP; + final RxBool autoLogin; + final Function() onLogin; + final FocusNode? userFocusNode; + const LoginWidgetUserPass({ + Key? key, + this.userFocusNode, + required this.username, + required this.pass, + required this.usernameMsg, + required this.passMsg, + required this.isInProgress, + required this.curOP, + required this.autoLogin, + required this.onLogin, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8.0), + DialogTextField( + title: '${translate("Username")}:', + controller: username, + focusNode: userFocusNode, + prefixIcon: Icon(Icons.account_circle_outlined), + errorText: usernameMsg), + DialogTextField( + title: '${translate("Password")}:', + obscureText: true, + controller: pass, + prefixIcon: Icon(Icons.lock_outline), + errorText: passMsg), + Obx(() => CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate("Remember me"), + ), + value: autoLogin.value, + onChanged: (v) { + if (v == null) return; + autoLogin.value = v; + }, + )), + Offstage( + offstage: !isInProgress, + child: const LinearProgressIndicator()), + const SizedBox(height: 12.0), + FittedBox( + child: + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Container( + height: 38, + width: 200, + child: Obx(() => ElevatedButton( + child: Text( + translate('Login'), + style: TextStyle(fontSize: 16), + ), + onPressed: + curOP.value.isEmpty || curOP.value == 'rustdesk' + ? () { + onLogin(); + } + : null, + )), + ), + ])), + ], + )); + } +} + +class DialogTextField extends StatelessWidget { + final String title; + final bool obscureText; + final String? errorText; + final String? helperText; + final Widget? prefixIcon; + final TextEditingController controller; + final FocusNode? focusNode; + + DialogTextField( + {Key? key, + this.focusNode, + this.obscureText = false, + this.errorText, + this.helperText, + this.prefixIcon, + required this.title, + required this.controller}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: TextField( + decoration: InputDecoration( + labelText: title, + border: const OutlineInputBorder(), + prefixIcon: prefixIcon, + helperText: helperText, + helperMaxLines: 8, + errorText: errorText), + controller: controller, + focusNode: focusNode, + autofocus: true, + obscureText: obscureText, + ), + ), + ], + ).paddingSymmetric(vertical: 4.0); + } +} + +/// common login dialog for desktop +/// call this directly +Future loginDialog() async { + var username = TextEditingController(); + var password = TextEditingController(); + final userFocusNode = FocusNode()..requestFocus(); + Timer(Duration(milliseconds: 100), () => userFocusNode..requestFocus()); + + String? usernameMsg; + String? passwordMsg; + var isInProgress = false; + final autoLogin = true.obs; + final RxString curOP = ''.obs; + + final res = await gFFI.dialogManager.show((setState, close) { + username.addListener(() { + if (usernameMsg != null) { + setState(() => usernameMsg = null); + } + }); + + password.addListener(() { + if (passwordMsg != null) { + setState(() => passwordMsg = null); + } + }); + + onDialogCancel() { + isInProgress = false; + close(false); + } + + onLogin() async { + // validate + if (username.text.isEmpty) { + setState(() => usernameMsg = translate('Username missed')); + return; + } + if (password.text.isEmpty) { + setState(() => passwordMsg = translate('Password missed')); + return; + } + curOP.value = 'rustdesk'; + setState(() => isInProgress = true); + try { + final resp = await gFFI.userModel.login(LoginRequest( + username: username.text, + password: password.text, + id: await bind.mainGetMyId(), + uuid: await bind.mainGetUuid(), + autoLogin: autoLogin.value, + type: HttpType.kAuthReqTypeAccount)); + + switch (resp.type) { + case HttpType.kAuthResTypeToken: + if (resp.access_token != null) { + await bind.mainSetLocalOption( + key: 'access_token', value: resp.access_token!); + close(true); + return; + } + break; + case HttpType.kAuthResTypeEmailCheck: + setState(() => isInProgress = false); + final res = await verificationCodeDialog(resp.user); + if (res == true) { + close(true); + return; + } + break; + default: + passwordMsg = "Failed, bad response from server"; + break; + } + } on RequestException catch (err) { + passwordMsg = translate(err.cause); + debugPrintStack(label: err.toString()); + } catch (err) { + passwordMsg = "Unknown Error: $err"; + debugPrintStack(label: err.toString()); + } + curOP.value = ''; + setState(() => isInProgress = false); + } + + return CustomAlertDialog( + title: Text(translate('Login')), + contentBoxConstraints: BoxConstraints(minWidth: 400), + content: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + height: 8.0, + ), + LoginWidgetUserPass( + username: username, + pass: password, + usernameMsg: usernameMsg, + passMsg: passwordMsg, + isInProgress: isInProgress, + curOP: curOP, + autoLogin: autoLogin, + onLogin: onLogin, + userFocusNode: userFocusNode, + ), + const SizedBox( + height: 8.0, + ), + Center( + child: Text( + translate('or'), + style: TextStyle(fontSize: 16), + )), + const SizedBox( + height: 8.0, + ), + LoginWidgetOP( + ops: [ + ConfigOP(op: 'Github', iconWidth: 20), + ConfigOP(op: 'Google', iconWidth: 20), + ConfigOP(op: 'Okta', iconWidth: 38), + ], + curOP: curOP, + cbLogin: (String username) { + gFFI.userModel.userName.value = username; + close(true); + }, + ), + ], + ), + actions: [msgBoxButton(translate('Close'), onDialogCancel)], + onCancel: onDialogCancel, + ); + }); + + if (res != null) { + // update ab and group status + await gFFI.abModel.pullAb(); + await gFFI.groupModel.pull(); + } + + return res; +} + +Future verificationCodeDialog(UserPayload? user) async { + var autoLogin = true; + var isInProgress = false; + String? errorText; + + final code = TextEditingController(); + final focusNode = FocusNode()..requestFocus(); + Timer(Duration(milliseconds: 100), () => focusNode..requestFocus()); + + final res = await gFFI.dialogManager.show((setState, close) { + bool validate() { + return code.text.length >= 6; + } + + code.addListener(() { + if (errorText != null) { + setState(() => errorText = null); + } + }); + + void onVerify() async { + if (!validate()) { + setState( + () => errorText = translate('Too short, at least 6 characters.')); + return; + } + setState(() => isInProgress = true); + + try { + final resp = await gFFI.userModel.login(LoginRequest( + verificationCode: code.text, + username: user?.name, + id: await bind.mainGetMyId(), + uuid: await bind.mainGetUuid(), + autoLogin: autoLogin, + type: HttpType.kAuthReqTypeEmailCode)); + + switch (resp.type) { + case HttpType.kAuthResTypeToken: + if (resp.access_token != null) { + await bind.mainSetLocalOption( + key: 'access_token', value: resp.access_token!); + close(true); + return; + } + break; + default: + errorText = "Failed, bad response from server"; + break; + } + } on RequestException catch (err) { + errorText = translate(err.cause); + debugPrintStack(label: err.toString()); + } catch (err) { + errorText = "Unknown Error: $err"; + debugPrintStack(label: err.toString()); + } + + setState(() => isInProgress = false); + } + + return CustomAlertDialog( + title: Text(translate("Verification code")), + contentBoxConstraints: BoxConstraints(maxWidth: 300), + content: Column( + children: [ + Offstage( + offstage: user?.email == null, + child: TextField( + decoration: InputDecoration( + labelText: "Email", + prefixIcon: Icon(Icons.email), + border: InputBorder.none), + readOnly: true, + controller: TextEditingController(text: user?.email), + )), + const SizedBox(height: 8), + DialogTextField( + title: '${translate("Verification code")}:', + controller: code, + errorText: errorText, + focusNode: focusNode, + helperText: translate('verification_tip'), + ), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Row(children: [ + Expanded(child: Text(translate("Trust this device"))) + ]), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = !autoLogin); + }, + ), + Offstage( + offstage: !isInProgress, + child: const LinearProgressIndicator()), + ], + ), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: onVerify, child: Text(translate("Verify"))), + ]); + }); + + return res; +} diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart new file mode 100644 index 000000000..65eaba40f --- /dev/null +++ b/flutter/lib/common/widgets/my_group.dart @@ -0,0 +1,225 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; + +class MyGroup extends StatefulWidget { + final EdgeInsets? menuPadding; + const MyGroup({Key? key, this.menuPadding}) : super(key: key); + + @override + State createState() { + return _MyGroupState(); + } +} + +class _MyGroupState extends State { + static final RxString selectedUser = ''.obs; + static final RxString searchUserText = ''.obs; + static TextEditingController searchUserController = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) => FutureBuilder( + future: buildBody(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return const Offstage(); + } + }); + + Future buildBody(BuildContext context) async { + return Obx(() { + if (gFFI.groupModel.userLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (gFFI.groupModel.userLoadError.isNotEmpty) { + return _buildShowError(gFFI.groupModel.userLoadError.value); + } + if (isDesktop) { + return _buildDesktop(); + } else { + return _buildMobile(); + } + }); + } + + Widget _buildShowError(String error) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate(error)), + TextButton( + onPressed: () { + gFFI.groupModel.pull(); + }, + child: Text(translate("Retry"))) + ], + )); + } + + Widget _buildDesktop() { + return Obx( + () => Row( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + width: 200, + height: double.infinity, + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + children: [ + _buildLeftHeader(), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(2)), + child: _buildUserContacts(), + ).marginSymmetric(vertical: 8.0), + ) + ], + ), + ), + ).marginOnly(right: 8.0), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: MyGroupPeerView( + menuPadding: widget.menuPadding, + initPeers: gFFI.groupModel.peersShow.value)), + ) + ], + ), + ); + } + + Widget _buildMobile() { + return Obx( + () => Column( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildLeftHeader(), + Container( + width: double.infinity, + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(4)), + child: _buildUserContacts(), + ).marginSymmetric(vertical: 8.0) + ], + ), + ), + ), + Divider(), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: MyGroupPeerView( + menuPadding: widget.menuPadding, + initPeers: gFFI.groupModel.peersShow.value)), + ) + ], + ), + ); + } + + Widget _buildLeftHeader() { + return Row( + children: [ + Expanded( + child: TextField( + controller: searchUserController, + onChanged: (value) { + searchUserText.value = value; + }, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.search_rounded, + color: Theme.of(context).hintColor, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 10), + hintText: translate("Search"), + hintStyle: + TextStyle(fontSize: 14, color: Theme.of(context).hintColor), + border: InputBorder.none, + isDense: true, + ), + )), + ], + ); + } + + Widget _buildUserContacts() { + return Obx(() { + return Column( + children: gFFI.groupModel.users + .where((p0) { + if (searchUserText.isNotEmpty) { + return p0.name.contains(searchUserText.value); + } + return true; + }) + .map((e) => _buildUserItem(e.name)) + .toList()); + }); + } + + Widget _buildUserItem(String username) { + return InkWell(onTap: () { + if (selectedUser.value != username) { + selectedUser.value = username; + gFFI.groupModel.pullUserPeers(username); + } + }, child: Obx( + () { + bool selected = selectedUser.value == username; + 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: [ + Icon(Icons.person_outline_rounded, color: Colors.grey, size: 16) + .marginOnly(right: 4), + Expanded(child: Text(username)), + ], + ).paddingSymmetric(vertical: 4), + ), + ); + }, + )).marginSymmetric(horizontal: 12); + } +} diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 449b67092..a98739606 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -321,6 +321,7 @@ enum CardType { fav, lan, ab, + grp, } abstract class BasePeerCard extends StatelessWidget { @@ -463,7 +464,7 @@ abstract class BasePeerCard extends StatelessWidget { ); } - /// Only avaliable on Windows. + /// Only available on Windows. @protected MenuEntryBase _createShortCutAction(String id) { return MenuEntryButton( @@ -684,6 +685,9 @@ abstract class BasePeerCard extends StatelessWidget { case CardType.ab: gFFI.abModel.pullAb(); break; + case CardType.grp: + gFFI.groupModel.pull(); + break; } } } @@ -937,6 +941,41 @@ class AddressBookPeerCard extends BasePeerCard { } } +class MyGroupPeerCard extends BasePeerCard { + MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super( + peer: peer, + cardType: CardType.grp, + menuPadding: menuPadding, + key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer), + _transferFileAction(context, peer.id), + ]; + if (isDesktop && peer.platform != 'Android') { + menuItems.add(_tcpTunnelingAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(_wolAction(peer.id)); + if (Platform.isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + menuItems.add(MenuEntryDivider()); + menuItems.add(_renameAction(peer.id)); + if (await bind.mainPeerHasPassword(id: peer.id)) { + menuItems.add(_unrememberPasswordAction(peer.id)); + } + return menuItems; + } +} + void _rdpDialog(String id, CardType card) async { String port, username; if (card == CardType.ab) { diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 1711e7b72..0c24fe7ea 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,9 +1,9 @@ -import 'dart:convert'; import 'dart:ui' as ui; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart'; +import 'package:flutter_hbb/common/widgets/my_group.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/consts.dart'; @@ -16,6 +16,101 @@ import 'package:get/get.dart'; import '../../common.dart'; import '../../models/platform_model.dart'; +const int groupTabIndex = 4; +const String defaultGroupTabname = 'Group'; + +class StatePeerTab { + final RxInt currentTab = 0.obs; + final RxInt tabHiddenFlag = 0.obs; + final RxList tabNames = [ + 'Recent Sessions', + 'Favorites', + 'Discovered', + 'Address Book', + defaultGroupTabname, + ].obs; + + StatePeerTab._() { + tabHiddenFlag.value = (int.tryParse( + bind.getLocalFlutterConfig(k: 'hidden-peer-card'), + radix: 2) ?? + 0); + var tabs = _notHiddenTabs(); + currentTab.value = + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0; + if (!tabs.contains(currentTab.value)) { + currentTab.value = 0; + } + } + static final StatePeerTab instance = StatePeerTab._(); + + check() { + var tabs = _notHiddenTabs(); + if (filterGroupCard()) { + if (currentTab.value == groupTabIndex) { + currentTab.value = + tabs.firstWhereOrNull((e) => e != groupTabIndex) ?? 0; + bind.setLocalFlutterConfig( + k: 'peer-tab-index', v: currentTab.value.toString()); + } + } else { + if (gFFI.userModel.isAdmin.isFalse && + gFFI.userModel.groupName.isNotEmpty) { + tabNames[groupTabIndex] = gFFI.userModel.groupName.value; + } else { + tabNames[groupTabIndex] = defaultGroupTabname; + } + if (tabs.contains(groupTabIndex) && + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == + groupTabIndex) { + currentTab.value = groupTabIndex; + } + } + } + + List currentTabs() { + var v = List.empty(growable: true); + for (int i = 0; i < tabNames.length; i++) { + if (!_isTabHidden(i) && !_isTabFilter(i)) { + v.add(i); + } + } + return v; + } + + bool filterGroupCard() { + if (gFFI.groupModel.users.isEmpty || + (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { + return true; + } else { + return false; + } + } + + bool _isTabHidden(int tabindex) { + return tabHiddenFlag & (1 << tabindex) != 0; + } + + bool _isTabFilter(int tabIndex) { + if (tabIndex == groupTabIndex) { + return filterGroupCard(); + } + return false; + } + + List _notHiddenTabs() { + var v = List.empty(growable: true); + for (int i = 0; i < tabNames.length; i++) { + if (!_isTabHidden(i)) { + v.add(i); + } + } + return v; + } +} + +final statePeerTab = StatePeerTab.instance; + class PeerTabPage extends StatefulWidget { const PeerTabPage({Key? key}) : super(key: key); @override @@ -23,10 +118,9 @@ class PeerTabPage extends StatefulWidget { } class _TabEntry { - final String name; final Widget widget; final Function() load; - _TabEntry(this.name, this.widget, this.load); + _TabEntry(this.widget, this.load); } EdgeInsets? _menuPadding() { @@ -35,65 +129,36 @@ EdgeInsets? _menuPadding() { class _PeerTabPageState extends State with SingleTickerProviderStateMixin { - late final RxInt tabHiddenFlag; - late final RxString currentTab; - late final RxList visibleOrderedTabs; final List<_TabEntry> entries = [ _TabEntry( - 'Recent Sessions', RecentPeersView( menuPadding: _menuPadding(), ), bind.mainLoadRecentPeers), _TabEntry( - 'Favorites', FavoritePeersView( menuPadding: _menuPadding(), ), bind.mainLoadFavPeers), _TabEntry( - 'Discovered', DiscoveredPeersView( menuPadding: _menuPadding(), ), bind.mainDiscover), _TabEntry( - 'Address Book', AddressBook( menuPadding: _menuPadding(), ), () => {}), + _TabEntry( + MyGroup( + menuPadding: _menuPadding(), + ), + () => {}), ]; @override void initState() { - tabHiddenFlag = (int.tryParse( - bind.getLocalFlutterConfig(k: 'hidden-peer-card'), - radix: 2) ?? - 0) - .obs; - currentTab = bind.getLocalFlutterConfig(k: 'current-peer-tab').obs; - visibleOrderedTabs = entries - .where((e) => !isTabHidden(e.name)) - .map((e) => e.name) - .toList() - .obs; - try { - final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); - if (conf.isNotEmpty) { - final json = jsonDecode(conf); - if (json is List) { - final List list = json.map((e) => e.toString()).toList(); - if (list.length == visibleOrderedTabs.length && - visibleOrderedTabs.every((e) => list.contains(e))) { - visibleOrderedTabs.value = list; - } - } - } - } catch (e) { - debugPrintStack(label: '$e'); - } - adjustTab(); final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type'); @@ -105,10 +170,11 @@ class _PeerTabPageState extends State super.initState(); } - Future handleTabSelection(String tabName) async { - currentTab.value = tabName; - await bind.setLocalFlutterConfig(k: 'current-peer-tab', v: tabName); - entries.firstWhereOrNull((e) => e.name == tabName)?.load(); + Future handleTabSelection(int tabIndex) async { + if (tabIndex < entries.length) { + statePeerTab.currentTab.value = tabIndex; + entries[tabIndex].load(); + } } @override @@ -149,65 +215,80 @@ class _PeerTabPageState extends State Widget _createSwitchBar(BuildContext context) { final textColor = Theme.of(context).textTheme.titleLarge?.color; return Obx(() { - int indexCounter = -1; - return ReorderableListView( - buildDefaultDragHandles: false, - onReorder: (oldIndex, newIndex) { - var list = visibleOrderedTabs.toList(); - if (oldIndex < newIndex) { - newIndex -= 1; - } - final String item = list.removeAt(oldIndex); - list.insert(newIndex, item); - bind.setLocalFlutterConfig( - k: 'peer-tab-order', v: jsonEncode(list)); - visibleOrderedTabs.value = list; - }, + var tabs = statePeerTab.currentTabs(); + return ListView( scrollDirection: Axis.horizontal, - shrinkWrap: true, - scrollController: ScrollController(), - children: visibleOrderedTabs.map((t) { - indexCounter++; - return ReorderableDragStartListener( - key: ValueKey(t), - index: indexCounter, - child: InkWell( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: currentTab.value == t - ? Theme.of(context).backgroundColor - : null, - borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), + physics: NeverScrollableScrollPhysics(), + controller: ScrollController(), + children: tabs.map((t) { + return InkWell( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: statePeerTab.currentTab.value == t + ? Theme.of(context).backgroundColor + : null, + borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), + ), + child: Align( + alignment: Alignment.center, + child: Text( + translatedTabname(t), + textAlign: TextAlign.center, + style: TextStyle( + height: 1, + fontSize: 14, + color: statePeerTab.currentTab.value == t + ? textColor + : textColor + ?..withOpacity(0.5)), ), - child: Align( - alignment: Alignment.center, - child: Text( - translate(t), - textAlign: TextAlign.center, - style: TextStyle( - height: 1, - fontSize: 14, - color: currentTab.value == t ? textColor : textColor - ?..withOpacity(0.5)), - ), - )), - onTap: () async => await handleTabSelection(t), - ), + )), + onTap: () async { + await handleTabSelection(t); + await bind.setLocalFlutterConfig( + k: 'peer-tab-index', v: t.toString()); + }, ); }).toList()); }); } + translatedTabname(int index) { + if (index < statePeerTab.tabNames.length) { + final name = statePeerTab.tabNames[index]; + if (index == groupTabIndex) { + if (name == defaultGroupTabname) { + return translate(name); + } else { + return name; + } + } else { + return translate(name); + } + } + assert(false); + return index.toString(); + } + Widget _createPeersView() { final verticalMargin = isDesktop ? 12.0 : 6.0; return Expanded( - child: Obx(() => - entries.firstWhereOrNull((e) => e.name == currentTab.value)?.widget ?? - visibleContextMenuListener(Center( - child: Text(translate('Right click to select tabs')), - ))).marginSymmetric(vertical: verticalMargin), - ); + child: Obx(() { + var tabs = statePeerTab.currentTabs(); + if (tabs.isEmpty) { + return visibleContextMenuListener(Center( + child: Text(translate('Right click to select tabs')), + )); + } else { + if (tabs.contains(statePeerTab.currentTab.value)) { + return entries[statePeerTab.currentTab.value].widget; + } else { + statePeerTab.currentTab.value = tabs[0]; + return entries[statePeerTab.currentTab.value].widget; + } + } + }).marginSymmetric(vertical: verticalMargin)); } Widget _createPeerViewTypeSwitch(BuildContext context) { @@ -240,22 +321,10 @@ class _PeerTabPageState extends State ); } - bool isTabHidden(String name) { - int index = entries.indexWhere((e) => e.name == name); - if (index >= 0) { - return tabHiddenFlag & (1 << index) != 0; - } - assert(false); - return false; - } - adjustTab() { - if (visibleOrderedTabs.isNotEmpty) { - if (!visibleOrderedTabs.contains(currentTab.value)) { - handleTabSelection(visibleOrderedTabs[0]); - } - } else { - currentTab.value = ''; + var tabs = statePeerTab.currentTabs(); + if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) { + statePeerTab.currentTab.value = tabs[0]; } } @@ -278,47 +347,44 @@ class _PeerTabPageState extends State } Widget visibleContextMenu(CancelFunc cancelFunc) { - final List menu = entries.asMap().entries.map((e) { - int bitMask = 1 << e.key; - return MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translate(e.value.name), - getter: () async { - return tabHiddenFlag.value & bitMask == 0; - }, - setter: (show) async { - if (show) { - tabHiddenFlag.value &= ~bitMask; - } else { - tabHiddenFlag.value |= bitMask; - } - await bind.setLocalFlutterConfig( - k: 'hidden-peer-card', v: tabHiddenFlag.value.toRadixString(2)); - visibleOrderedTabs.removeWhere((e) => isTabHidden(e)); - visibleOrderedTabs.addAll(entries - .where((e) => - !visibleOrderedTabs.contains(e.name) && - !isTabHidden(e.name)) - .map((e) => e.name) - .toList()); - await bind.setLocalFlutterConfig( - k: 'peer-tab-order', v: jsonEncode(visibleOrderedTabs)); - cancelFunc(); - adjustTab(); - }); - }).toList(); - return mod_menu.PopupMenu( - items: menu - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: MyTheme.accent, - height: 20.0, - dividerHeight: 12.0, - ))) - .expand((i) => i) - .toList(), - ); + return Obx(() { + final List menu = List.empty(growable: true); + for (int i = 0; i < statePeerTab.tabNames.length; i++) { + if (i == groupTabIndex && statePeerTab.filterGroupCard()) { + continue; + } + int bitMask = 1 << i; + menu.add(MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translatedTabname(i), + getter: () async { + return statePeerTab.tabHiddenFlag & bitMask == 0; + }, + setter: (show) async { + if (show) { + statePeerTab.tabHiddenFlag.value &= ~bitMask; + } else { + statePeerTab.tabHiddenFlag.value |= bitMask; + } + await bind.setLocalFlutterConfig( + k: 'hidden-peer-card', + v: statePeerTab.tabHiddenFlag.value.toRadixString(2)); + cancelFunc(); + adjustTab(); + })); + } + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: MyTheme.accent, + height: 20.0, + dividerHeight: 12.0, + ))) + .expand((i) => i) + .toList()); + }); } } diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 6e52bfeb8..9c98f24b8 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -326,3 +326,21 @@ class AddressBookPeersView extends BasePeersView { return true; } } + +class MyGroupPeerView extends BasePeersView { + MyGroupPeerView( + {Key? key, + EdgeInsets? menuPadding, + ScrollController? scrollController, + required List initPeers}) + : super( + key: key, + name: 'my group peer', + loadEvent: 'load_my_group_peers', + peerCardBuilder: (Peer peer) => MyGroupPeerCard( + peer: peer, + menuPadding: menuPadding, + ), + initPeers: initPeers, + ); +} diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 3a79d24fb..017850cf5 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -9,11 +9,12 @@ class RawKeyFocusScope extends StatelessWidget { final InputModel inputModel; final Widget child; - RawKeyFocusScope( - {this.focusNode, - this.onFocusChange, - required this.inputModel, - required this.child}); + RawKeyFocusScope({ + this.focusNode, + this.onFocusChange, + required this.inputModel, + required this.child, + }); @override Widget build(BuildContext context) { @@ -35,11 +36,15 @@ class RawPointerMouseRegion extends StatelessWidget { final MouseCursor? cursor; final PointerEnterEventListener? onEnter; final PointerExitEventListener? onExit; + final PointerDownEventListener? onPointerDown; + final PointerUpEventListener? onPointerUp; RawPointerMouseRegion( {this.onEnter, this.onExit, this.cursor, + this.onPointerDown, + this.onPointerUp, required this.inputModel, required this.child}); @@ -47,8 +52,14 @@ class RawPointerMouseRegion extends StatelessWidget { Widget build(BuildContext context) { return Listener( onPointerHover: inputModel.onPointHoverImage, - onPointerDown: inputModel.onPointDownImage, - onPointerUp: inputModel.onPointUpImage, + onPointerDown: (evt) { + onPointerDown?.call(evt); + inputModel.onPointDownImage(evt); + }, + onPointerUp: (evt) { + onPointerUp?.call(evt); + inputModel.onPointUpImage(evt); + }, onPointerMove: inputModel.onPointMoveImage, onPointerSignal: inputModel.onPointerSignalImage, /* diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 50e7f594b..7aa200ae9 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -100,6 +100,8 @@ const kRemoteImageQualityLow = 'low'; /// [kRemoteImageQualityCustom] Custom image quality. const kRemoteImageQualityCustom = 'custom'; +const kIgnoreDpi = true; + /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels /// see [LogicalKeyboardKey.keyLabel] const Map logicalKeyMap = { diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 7500fe99e..85749a256 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -6,7 +6,6 @@ import 'dart:io'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:get/get.dart'; @@ -16,7 +15,6 @@ import 'package:window_manager/window_manager.dart'; import '../../common.dart'; import '../../common/formatter/id_formatter.dart'; import '../../common/widgets/peer_tab_page.dart'; -import '../../common/widgets/peers_view.dart'; import '../../models/platform_model.dart'; import '../widgets/button.dart'; @@ -172,6 +170,7 @@ class _ConnectionPageState extends State Expanded( child: Obx( () => TextField( + maxLength: 90, autocorrect: false, enableSuggestions: false, keyboardType: TextInputType.visiblePassword, @@ -179,12 +178,13 @@ class _ConnectionPageState extends State style: const TextStyle( fontFamily: 'WorkSans', fontSize: 22, - height: 1, + height: 1.25, ), maxLines: 1, cursorColor: Theme.of(context).textTheme.titleLarge?.color, decoration: InputDecoration( + counterText: '', hintText: _idInputFocused.value ? null : translate('Enter Remote ID'), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index e0cb5a676..fd9814cc2 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -42,6 +42,7 @@ class _DesktopHomePageState extends State var svcStopped = false.obs; var watchIsCanScreenRecording = false; var watchIsProcessTrust = false; + var watchIsInputMonitoring = false; Timer? _updateTimer; @override @@ -334,6 +335,12 @@ class _DesktopHomePageState extends State bind.mainIsProcessTrusted(prompt: true); watchIsProcessTrust = true; }, help: 'Help', link: translate("doc_mac_permission")); + } else if (!bind.mainIsCanInputMonitoring(prompt: false)) { + return buildInstallCard("Permissions", "config_input", "Configure", + () async { + bind.mainIsCanInputMonitoring(prompt: true); + watchIsInputMonitoring = true; + }, help: 'Help', link: translate("doc_mac_permission")); } else if (!svcStopped.value && bind.mainIsInstalled() && !bind.mainIsInstalledDaemon(prompt: false)) { @@ -438,7 +445,6 @@ class _DesktopHomePageState extends State @override void initState() { super.initState(); - bind.mainStartGrabKeyboard(); _updateTimer = periodic_immediate(const Duration(seconds: 1), () async { await gFFI.serverModel.fetchID(); final url = await bind.mainGetSoftwareUpdateUrl(); @@ -468,6 +474,12 @@ class _DesktopHomePageState extends State setState(() {}); } } + if (watchIsInputMonitoring) { + if (bind.mainIsCanInputMonitoring(prompt: false)) { + watchIsInputMonitoring = false; + setState(() {}); + } + } }); Get.put(svcStopped, tag: 'stop-service'); rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); @@ -501,9 +513,9 @@ class _DesktopHomePageState extends State } else if (call.method == kWindowActionRebuild) { reloadCurrentWindow(); } else if (call.method == kWindowEventShow) { - rustDeskWinManager.registerActiveWindow(call.arguments["id"]); + await rustDeskWinManager.registerActiveWindow(call.arguments["id"]); } else if (call.method == kWindowEventHide) { - rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]); + await rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]); } else if (call.method == kWindowConnect) { await connectMainDesktop( call.arguments['id'], diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 06cabebe7..ac92da14c 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -8,7 +8,6 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; -import 'package:flutter_hbb/desktop/widgets/login.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; @@ -18,6 +17,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import '../../common/widgets/dialog.dart'; +import '../../common/widgets/login.dart'; const double _kTabWidth = 235; const double _kTabHeight = 42; @@ -125,6 +125,7 @@ class _DesktopSettingPageState extends State scrollController: controller, child: PageView( controller: controller, + physics: NeverScrollableScrollPhysics(), children: const [ _General(), _Safety(), @@ -273,6 +274,15 @@ class _GeneralState extends State<_General> { _OptionCheckBox(context, 'Confirm before closing multiple tabs', 'enable-confirm-closing-tabs'), _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'), + if (Platform.isLinux) + Tooltip( + message: translate('software_render_tip'), + child: _OptionCheckBox( + context, + "Always use software rendering", + 'allow-always-software-render', + ), + ) ]); } @@ -932,6 +942,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { return false; } } + final old = await bind.mainGetOption(key: 'custom-rendezvous-server'); + if (old.isNotEmpty && old != idServer) { + await gFFI.userModel.logOut(); + } // should set one by one await bind.mainSetOption( key: 'custom-rendezvous-server', value: idServer); @@ -954,23 +968,17 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { import() { Clipboard.getData(Clipboard.kTextPlain).then((value) { - TextEditingController mytext = TextEditingController(); - String? aNullableString = ''; - aNullableString = value?.text; - mytext.text = aNullableString.toString(); - if (mytext.text.isNotEmpty) { + final text = value?.text; + if (text != null && text.isNotEmpty) { try { - Map config = jsonDecode(mytext.text); - if (config.containsKey('IdServer')) { - String id = config['IdServer'] ?? ''; - String relay = config['RelayServer'] ?? ''; - String api = config['ApiServer'] ?? ''; - String key = config['Key'] ?? ''; - idController.text = id; - relayController.text = relay; - apiController.text = api; - keyController.text = key; - Future success = set(id, relay, api, key); + final sc = ServerConfig.decode(text); + if (sc.idServer.isNotEmpty) { + idController.text = sc.idServer; + relayController.text = sc.relayServer; + apiController.text = sc.apiServer; + keyController.text = sc.key; + Future success = + set(sc.idServer, sc.relayServer, sc.apiServer, sc.key); success.then((value) { if (value) { showToast( @@ -992,12 +1000,15 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } export() { - Map config = {}; - config['IdServer'] = idController.text.trim(); - config['RelayServer'] = relayController.text.trim(); - config['ApiServer'] = apiController.text.trim(); - config['Key'] = keyController.text.trim(); - Clipboard.setData(ClipboardData(text: jsonEncode(config))); + final text = ServerConfig( + idServer: idController.text, + relayServer: relayController.text, + apiServer: apiController.text, + key: keyController.text) + .encode(); + debugPrint("ServerConfig export: $text"); + + Clipboard.setData(ClipboardData(text: text)); showToast(translate('Export server configuration successfully')); } @@ -1059,21 +1070,13 @@ class _AccountState extends State<_Account> { } Widget accountAction() { - return _futureBuilder(future: () async { - return await gFFI.userModel.getUserName(); - }(), hasData: (_) { - return Obx(() => _Button( - gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', - () => { - gFFI.userModel.userName.value.isEmpty - ? loginDialog().then((success) { - if (success) { - gFFI.abModel.pullAb(); - } - }) - : gFFI.userModel.logOut() - })); - }); + return Obx(() => _Button( + gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', + () => { + gFFI.userModel.userName.value.isEmpty + ? loginDialog() + : gFFI.userModel.logOut() + })); } } @@ -1103,29 +1106,31 @@ class _AboutState extends State<_About> { child: SingleChildScrollView( controller: scrollController, physics: NeverScrollableScrollPhysics(), - child: _Card(title: 'About RustDesk', children: [ + child: _Card(title: '${translate('About')} RustDesk', children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox( height: 8.0, ), - Text('Version: $version').marginSymmetric(vertical: 4.0), - Text('Build Date: $buildDate').marginSymmetric(vertical: 4.0), + Text('${translate('Version')}: $version') + .marginSymmetric(vertical: 4.0), + Text('${translate('Build Date')}: $buildDate') + .marginSymmetric(vertical: 4.0), InkWell( onTap: () { launchUrlString('https://rustdesk.com/privacy'); }, - child: const Text( - 'Privacy Statement', + child: Text( + translate('Privacy Statement'), style: linkStyle, ).marginSymmetric(vertical: 4.0)), InkWell( onTap: () { launchUrlString('https://rustdesk.com'); }, - child: const Text( - 'Website', + child: Text( + translate('Website'), style: linkStyle, ).marginSymmetric(vertical: 4.0)), Container( @@ -1142,8 +1147,8 @@ class _AboutState extends State<_About> { 'Copyright © 2022 Purslane Ltd.\n$license', style: const TextStyle(color: Colors.white), ), - const Text( - 'Made with heart in this chaotic world!', + Text( + translate('Slogan_tip'), style: TextStyle( fontWeight: FontWeight.w800, color: Colors.white), @@ -1227,7 +1232,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key, ref.value = option; if (reverse) option = !option; String value = bool2option(key, option); - bind.mainSetOption(key: key, value: value); + await bind.mainSetOption(key: key, value: value); update?.call(); } } @@ -1455,6 +1460,8 @@ _LabeledTextField( enabled: enabled, obscureText: secure, decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 15), errorText: errorText.isNotEmpty ? errorText : null), style: TextStyle( color: _disabledTextColor(context, enabled), diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 9d8ef6f7a..60b22a516 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; +import 'package:flutter_hbb/desktop/widgets/list_search_action_listener.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:get/get.dart'; @@ -32,6 +33,18 @@ enum LocationStatus { fileSearchBar } +/// The status of currently focused scope of the mouse +enum MouseFocusScope { + /// Mouse is in local field. + local, + + /// Mouse is in remote field. + remote, + + /// Mouse is not in local field, remote neither. + none +} + class FileManagerPage extends StatefulWidget { const FileManagerPage({Key? key, required this.id}) : super(key: key); final String id; @@ -55,6 +68,11 @@ class _FileManagerPageState extends State final _searchTextRemote = "".obs; final _breadCrumbScrollerLocal = ScrollController(); final _breadCrumbScrollerRemote = ScrollController(); + final _mouseFocusScope = Rx(MouseFocusScope.none); + final _keyboardNodeLocal = FocusNode(debugLabel: "keyboardNodeLocal"); + final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote"); + final _listSearchBufferLocal = TimeoutStringBuffer(); + final _listSearchBufferRemote = TimeoutStringBuffer(); /// [_lastClickTime], [_lastClickEntry] help to handle double click int _lastClickTime = @@ -197,6 +215,7 @@ class _FileManagerPageState extends State } Widget body({bool isLocal = false}) { + final scrollController = ScrollController(); return Container( decoration: BoxDecoration(border: Border.all(color: Colors.black26)), margin: const EdgeInsets.all(16.0), @@ -217,8 +236,8 @@ class _FileManagerPageState extends State children: [ Expanded( child: SingleChildScrollView( - controller: ScrollController(), - child: _buildDataTable(context, isLocal), + controller: scrollController, + child: _buildDataTable(context, isLocal, scrollController), ), ) ], @@ -228,7 +247,9 @@ class _FileManagerPageState extends State ); } - Widget _buildDataTable(BuildContext context, bool isLocal) { + Widget _buildDataTable( + BuildContext context, bool isLocal, ScrollController scrollController) { + const rowHeight = 25.0; final fd = model.getCurrentDir(isLocal); final entries = fd.entries; final sortIndex = (SortBy style) { @@ -246,130 +267,219 @@ class _FileManagerPageState extends State final sortAscending = isLocal ? model.localSortAscending : model.remoteSortAscending; - return ObxValue( - (searchText) { - final filteredEntries = searchText.isNotEmpty - ? entries.where((element) { - return element.name.contains(searchText.value); - }).toList(growable: false) - : entries; - return DataTable( - key: ValueKey(isLocal ? 0 : 1), - showCheckboxColumn: false, - dataRowHeight: 25, - headingRowHeight: 30, - horizontalMargin: 8, - columnSpacing: 8, - showBottomBorder: true, - sortColumnIndex: sortIndex, - sortAscending: sortAscending, - columns: [ - DataColumn( - label: Text( - translate("Name"), - ).marginSymmetric(horizontal: 4), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.name, - isLocal: isLocal, ascending: ascending); - }), - DataColumn( - label: Text( - translate("Modified"), - ), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.modified, - isLocal: isLocal, ascending: ascending); - }), - DataColumn( - label: Text(translate("Size")), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.size, - isLocal: isLocal, ascending: ascending); - }), - ], - rows: filteredEntries.map((entry) { - final sizeStr = - entry.isFile ? readableFileSize(entry.size.toDouble()) : ""; - final lastModifiedStr = entry.isDrive - ? " " - : "${entry.lastModified().toString().replaceAll(".000", "")} "; - return DataRow( - key: ValueKey(entry.name), - onSelectChanged: (s) { - _onSelectedChanged(getSelectedItems(isLocal), filteredEntries, - entry, isLocal); - }, - selected: getSelectedItems(isLocal).contains(entry), - cells: [ - DataCell( - Container( - width: 200, - child: Tooltip( - waitDuration: Duration(milliseconds: 500), - message: entry.name, - child: Row(children: [ - entry.isDrive - ? Image( - image: iconHardDrive, - fit: BoxFit.scaleDown, + return MouseRegion( + onEnter: (evt) { + _mouseFocusScope.value = + isLocal ? MouseFocusScope.local : MouseFocusScope.remote; + if (isLocal) { + _keyboardNodeLocal.requestFocus(); + } else { + _keyboardNodeRemote.requestFocus(); + } + }, + onExit: (evt) { + _mouseFocusScope.value = MouseFocusScope.none; + }, + child: ListSearchActionListener( + node: isLocal ? _keyboardNodeLocal : _keyboardNodeRemote, + buffer: isLocal ? _listSearchBufferLocal : _listSearchBufferRemote, + onNext: (buffer) { + debugPrint("searching next for $buffer"); + assert(buffer.length == 1); + final selectedEntries = getSelectedItems(isLocal); + assert(selectedEntries.length <= 1); + var skipCount = 0; + if (selectedEntries.items.isNotEmpty) { + final index = entries.indexOf(selectedEntries.items.first); + if (index < 0) { + return; + } + skipCount = index + 1; + } + var searchResult = entries + .skip(skipCount) + .where((element) => element.name.startsWith(buffer)); + if (searchResult.isEmpty) { + // cannot find next, lets restart search from head + searchResult = + entries.where((element) => element.name.startsWith(buffer)); + } + if (searchResult.isEmpty) { + setState(() { + getSelectedItems(isLocal).clear(); + }); + return; + } + _jumpToEntry( + isLocal, searchResult.first, scrollController, rowHeight, buffer); + }, + onSearch: (buffer) { + debugPrint("searching for $buffer"); + final selectedEntries = getSelectedItems(isLocal); + final searchResult = + entries.where((element) => element.name.startsWith(buffer)); + selectedEntries.clear(); + if (searchResult.isEmpty) { + setState(() { + getSelectedItems(isLocal).clear(); + }); + return; + } + _jumpToEntry( + isLocal, searchResult.first, scrollController, rowHeight, buffer); + }, + child: ObxValue( + (searchText) { + final filteredEntries = searchText.isNotEmpty + ? entries.where((element) { + return element.name.contains(searchText.value); + }).toList(growable: false) + : entries; + return DataTable( + key: ValueKey(isLocal ? 0 : 1), + showCheckboxColumn: false, + dataRowHeight: rowHeight, + headingRowHeight: 30, + horizontalMargin: 8, + columnSpacing: 8, + showBottomBorder: true, + sortColumnIndex: sortIndex, + sortAscending: sortAscending, + columns: [ + DataColumn( + label: Text( + translate("Name"), + ).marginSymmetric(horizontal: 4), + onSort: (columnIndex, ascending) { + model.changeSortStyle(SortBy.name, + isLocal: isLocal, ascending: ascending); + }), + DataColumn( + label: Text( + translate("Modified"), + ), + onSort: (columnIndex, ascending) { + model.changeSortStyle(SortBy.modified, + isLocal: isLocal, ascending: ascending); + }), + DataColumn( + label: Text(translate("Size")), + onSort: (columnIndex, ascending) { + model.changeSortStyle(SortBy.size, + isLocal: isLocal, ascending: ascending); + }), + ], + rows: filteredEntries.map((entry) { + final sizeStr = + entry.isFile ? readableFileSize(entry.size.toDouble()) : ""; + final lastModifiedStr = entry.isDrive + ? " " + : "${entry.lastModified().toString().replaceAll(".000", "")} "; + return DataRow( + key: ValueKey(entry.name), + onSelectChanged: (s) { + _onSelectedChanged(getSelectedItems(isLocal), + filteredEntries, entry, isLocal); + }, + selected: getSelectedItems(isLocal).contains(entry), + cells: [ + DataCell( + Container( + width: 200, + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: entry.name, + child: Row(children: [ + entry.isDrive + ? Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)) + .paddingAll(4) + : Icon( + entry.isFile + ? Icons.feed_outlined + : Icons.folder, + size: 20, color: Theme.of(context) .iconTheme .color - ?.withOpacity(0.7)) - .paddingAll(4) - : Icon( - entry.isFile - ? Icons.feed_outlined - : Icons.folder, - size: 20, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7), - ).marginSymmetric(horizontal: 2), - Expanded( - child: Text(entry.name, - overflow: TextOverflow.ellipsis)) - ]), - )), - onTap: () { - final items = getSelectedItems(isLocal); + ?.withOpacity(0.7), + ).marginSymmetric(horizontal: 2), + Expanded( + child: Text(entry.name, + overflow: TextOverflow.ellipsis)) + ]), + )), + onTap: () { + final items = getSelectedItems(isLocal); - // handle double click - if (_checkDoubleClick(entry)) { - openDirectory(entry.path, isLocal: isLocal); - items.clear(); - return; - } - _onSelectedChanged( - items, filteredEntries, entry, isLocal); - }, - ), - DataCell(FittedBox( - child: Tooltip( + // handle double click + if (_checkDoubleClick(entry)) { + openDirectory(entry.path, isLocal: isLocal); + items.clear(); + return; + } + _onSelectedChanged( + items, filteredEntries, entry, isLocal); + }, + ), + DataCell(FittedBox( + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )))), + DataCell(Tooltip( waitDuration: Duration(milliseconds: 500), - message: lastModifiedStr, + message: sizeStr, child: Text( - lastModifiedStr, + sizeStr, + overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )))), - DataCell(Tooltip( - waitDuration: Duration(milliseconds: 500), - message: sizeStr, - child: Text( - sizeStr, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 10, color: MyTheme.darkGray), - ))), - ]); - }).toList(growable: false), - ); - }, - isLocal ? _searchTextLocal : _searchTextRemote, + fontSize: 10, color: MyTheme.darkGray), + ))), + ]); + }).toList(growable: false), + ); + }, + isLocal ? _searchTextLocal : _searchTextRemote, + ), + ), ); } + void _jumpToEntry(bool isLocal, Entry entry, + ScrollController scrollController, double rowHeight, String buffer) { + final entries = model.getCurrentDir(isLocal).entries; + final index = entries.indexOf(entry); + if (index == -1) { + debugPrint("entry is not valid: ${entry.path}"); + } + final selectedEntries = getSelectedItems(isLocal); + final searchResult = + entries.where((element) => element.name.startsWith(buffer)); + selectedEntries.clear(); + if (searchResult.isEmpty) { + return; + } + final offset = min( + max(scrollController.position.minScrollExtent, + entries.indexOf(searchResult.first) * rowHeight), + scrollController.position.maxScrollExtent); + scrollController.jumpTo(offset); + setState(() { + selectedEntries.add(isLocal, searchResult.first); + debugPrint("focused on ${searchResult.first.name}"); + }); + } + void _onSelectedChanged(SelectedItems selectedItems, List entries, Entry entry, bool isLocal) { final isCtrlDown = RawKeyboard.instance.keysPressed @@ -872,6 +982,8 @@ class _FileManagerPageState extends State }, dismissOnClicked: true)); } + } catch (e) { + debugPrint("buildBread fetchDirectory err=$e"); } finally { if (!isLocal) { _ffi.dialogManager.dismissByTag(loadingTag); @@ -1015,4 +1127,14 @@ class _FileManagerPageState extends State } model.sendFiles(items, isRemote: false); } + + void refocusKeyboardListener(bool isLocal) { + Future.delayed(Duration.zero, () { + if (isLocal) { + _keyboardNodeLocal.requestFocus(); + } else { + _keyboardNodeRemote.requestFocus(); + } + }); + } } diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 7b3e0fe82..55a5bbaef 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -2,9 +2,12 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; +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_custom_cursor/cursor_manager.dart' + as custom_cursor_manager; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; @@ -20,6 +23,7 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import '../widgets/remote_menubar.dart'; +import '../widgets/kb_layout_type_chooser.dart'; bool _isCustomCursorInited = false; final SimpleWrapper _firstEnterImage = SimpleWrapper(false); @@ -46,17 +50,17 @@ class RemotePage extends StatefulWidget { } class _RemotePageState extends State - with AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin, MultiWindowListener { Timer? _timer; String keyboardMode = "legacy"; + bool _isWindowBlur = false; final _cursorOverImage = false.obs; late RxBool _showRemoteCursor; late RxBool _zoomCursor; late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; - final FocusNode _rawKeyFocusNode = FocusNode(); - var _imageFocused = false; + final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); Function(bool)? _onEnterOrLeaveImage4Menubar; @@ -92,6 +96,10 @@ class _RemotePageState extends State _initStates(widget.id); _ffi = FFI(); Get.put(_ffi, tag: widget.id); + _ffi.imageModel.addCallbackOnFirstImage((String peerId) { + showKBLayoutTypeChooserIfNeeded( + _ffi.ffiModel.pi.platform, _ffi.dialogManager); + }); _ffi.start(widget.id); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); @@ -101,7 +109,6 @@ class _RemotePageState extends State if (!Platform.isLinux) { Wakelock.enable(); } - _rawKeyFocusNode.requestFocus(); _ffi.ffiModel.updateEventListener(widget.id); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // Session option should be set after models.dart/FFI.start @@ -109,22 +116,59 @@ class _RemotePageState extends State id: widget.id, arg: 'show-remote-cursor'); _zoomCursor.value = bind.sessionGetToggleOptionSync(id: widget.id, arg: 'zoom-cursor'); - if (!_isCustomCursorInited) { - customCursorController.registerNeedUpdateCursorCallback( - (String? lastKey, String? currentKey) async { - if (_firstEnterImage.value) { - _firstEnterImage.value = false; - return true; - } - return lastKey == null || lastKey != currentKey; - }); - _isCustomCursorInited = true; + 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; + // } + } + + @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 (Platform.isWindows) { + _isWindowBlur = true; + // unfocus the primary-focus when the whole window is lost focus, + // and let OS to handle events instead. + _rawKeyFocusNode.unfocus(); + } + } + + @override + void onWindowFocus() { + super.onWindowFocus(); + // See [onWindowBlur]. + if (Platform.isWindows) { + _isWindowBlur = false; + } + } + + @override + void onWindowRestore() { + super.onWindowRestore(); + // On windows, we use `onWindowRestore` way to handle window restore from + // a minimized state. + if (Platform.isWindows) { + _isWindowBlur = false; } } @override void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); + // ensure we leave this session, this is a double check + bind.sessionEnterOrLeave(id: widget.id, enter: false); + DesktopMultiWindow.removeListener(this); _ffi.dialogManager.hideMobileActionsOverlay(); _ffi.recordingModel.onClose(); _rawKeyFocusNode.dispose(); @@ -153,8 +197,23 @@ class _RemotePageState extends State color: Colors.black, child: RawKeyFocusScope( focusNode: _rawKeyFocusNode, - onFocusChange: (bool v) { - _imageFocused = v; + onFocusChange: (bool imageFocused) { + debugPrint( + "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); + // See [onWindowBlur]. + if (Platform.isWindows) { + if (_isWindowBlur) { + imageFocused = false; + Future.delayed(Duration.zero, () { + _rawKeyFocusNode.unfocus(); + }); + } + if (imageFocused) { + _ffi.inputModel.enterOrLeave(true); + } else { + _ffi.inputModel.enterOrLeave(false); + } + } }, inputModel: _ffi.inputModel, child: getBodyForDesktop(context))); @@ -181,9 +240,6 @@ class _RemotePageState extends State } void enterView(PointerEnterEvent evt) { - if (!_imageFocused) { - _rawKeyFocusNode.requestFocus(); - } _cursorOverImage.value = true; _firstEnterImage.value = true; if (_onEnterOrLeaveImage4Menubar != null) { @@ -193,7 +249,13 @@ class _RemotePageState extends State // } } - _ffi.inputModel.enterOrLeave(true); + // See [onWindowBlur]. + if (!Platform.isWindows) { + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + bind.sessionEnterOrLeave(id: widget.id, enter: true); + } } void leaveView(PointerExitEvent evt) { @@ -206,7 +268,10 @@ class _RemotePageState extends State // } } - _ffi.inputModel.enterOrLeave(false); + // See [onWindowBlur]. + if (!Platform.isWindows) { + bind.sessionEnterOrLeave(id: widget.id, enter: false); + } } Widget getBodyForDesktop(BuildContext context) { @@ -228,6 +293,21 @@ class _RemotePageState extends State listenerBuilder: (child) => RawPointerMouseRegion( onEnter: enterView, onExit: leaveView, + 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, ), @@ -235,9 +315,9 @@ class _RemotePageState extends State })) ]; - if (!_ffi.canvasModel.cursorEmbeded) { - paints.add(Obx(() => Visibility( - visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue, + if (!_ffi.canvasModel.cursorEmbedded) { + paints.add(Obx(() => Offstage( + offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse, child: CursorPaint( id: widget.id, zoomCursor: _zoomCursor, @@ -302,7 +382,7 @@ class _ImagePaintState extends State { mouseRegion({child}) => Obx(() => MouseRegion( cursor: cursorOverImage.isTrue - ? c.cursorEmbeded + ? c.cursorEmbedded ? SystemMouseCursors.none : keyboardEnabled.isTrue ? (() { @@ -322,35 +402,36 @@ class _ImagePaintState extends State { onHover: (evt) {}, child: child)); - if (c.scrollStyle == ScrollStyle.scrollbar) { + if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { final imageWidth = c.getDisplayWidth() * s; final imageHeight = c.getDisplayHeight() * s; + final imageSize = Size(imageWidth, imageHeight); final imageWidget = CustomPaint( - size: Size(imageWidth, imageHeight), + size: imageSize, painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), ); return NotificationListener( - onNotification: (notification) { - final percentX = _horizontal.hasClients - ? _horizontal.position.extentBefore / - (_horizontal.position.extentBefore + - _horizontal.position.extentInside + - _horizontal.position.extentAfter) - : 0.0; - final percentY = _vertical.hasClients - ? _vertical.position.extentBefore / - (_vertical.position.extentBefore + - _vertical.position.extentInside + - _vertical.position.extentAfter) - : 0.0; - c.setScrollPercent(percentX, percentY); - return false; - }, - child: mouseRegion( - child: _buildCrossScrollbar(context, _buildListener(imageWidget), - Size(imageWidth, imageHeight))), - ); + onNotification: (notification) { + final percentX = _horizontal.hasClients + ? _horizontal.position.extentBefore / + (_horizontal.position.extentBefore + + _horizontal.position.extentInside + + _horizontal.position.extentAfter) + : 0.0; + final percentY = _vertical.hasClients + ? _vertical.position.extentBefore / + (_vertical.position.extentBefore + + _vertical.position.extentInside + + _vertical.position.extentAfter) + : 0.0; + c.setScrollPercent(percentX, percentY); + return false; + }, + child: mouseRegion( + child: Obx(() => _buildCrossScrollbarFromLayout( + context, _buildListener(imageWidget), c.size, imageSize)), + )); } else { final imageWidget = CustomPaint( size: Size(c.size.width, c.size.height), @@ -366,15 +447,23 @@ class _ImagePaintState extends State { return MouseCursor.defer; } else { final key = cache.updateGetKey(scale, zoomCursor.value); - cursor.addKey(key); - return FlutterCustomMemoryImageCursor( - pixbuf: cache.data, - key: key, - hotx: cache.hotx, - hoty: cache.hoty, - imageWidth: (cache.width * cache.scale).toInt(), - imageHeight: (cache.height * cache.scale).toInt(), - ); + if (!cursor.cachedKeys.contains(key)) { + debugPrint("Register custom cursor with key $key"); + // [Safety] + // It's ok to call async registerCursor in current synchronous context, + // because activating the cursor is also an async call and will always + // be executed after this. + custom_cursor_manager.CursorManager.instance + .registerCursor(custom_cursor_manager.CursorData() + ..buffer = cache.data! + ..height = (cache.height * cache.scale).toInt() + ..width = (cache.width * cache.scale).toInt() + ..hotX = cache.hotx + ..hotY = cache.hoty + ..name = key); + cursor.addKey(key); + } + return FlutterCustomMemoryImageCursor(key: key); } } @@ -477,24 +566,6 @@ class _ImagePaintState extends State { return widget; } - Widget _buildCrossScrollbar(BuildContext context, Widget child, Size size) { - var layoutSize = MediaQuery.of(context).size; - // If minimized, w or h may be negative here. - final w = layoutSize.width - kWindowBorderWidth * 2; - final h = - layoutSize.height - kWindowBorderWidth * 2 - kDesktopRemoteTabBarHeight; - layoutSize = Size( - w < 0 ? 0 : w, - h < 0 ? 0 : h, - ); - bool overflow = - layoutSize.width < size.width || layoutSize.height < size.height; - return overflow - ? Obx(() => - _buildCrossScrollbarFromLayout(context, child, layoutSize, size)) - : _buildCrossScrollbarFromLayout(context, child, layoutSize, size); - } - Widget _buildListener(Widget child) { if (listenerBuilder != null) { return listenerBuilder!(child); @@ -529,7 +600,8 @@ class CursorPaint extends StatelessWidget { double cx = c.x; double cy = c.y; - if (c.scrollStyle == ScrollStyle.scrollbar) { + if (c.viewStyle.style == kRemoteViewStyleOriginal && + c.scrollStyle == ScrollStyle.scrollbar) { final d = c.parent.target!.ffiModel.display; final imageWidth = d.width * c.scale; final imageHeight = d.height * c.scale; @@ -538,7 +610,7 @@ class CursorPaint extends StatelessWidget { } double x = (m.x - hotx) * c.scale + cx; - double y = (m.y - hoty) * c.scale + cx; + double y = (m.y - hoty) * c.scale + cy; double scale = 1.0; if (zoomCursor.isTrue) { x = m.x - hotx + cx / c.scale; diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 713c3d13c..604787290 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -257,7 +257,7 @@ class _ConnectionTabPageState extends State { ), ]); - if (!ffi.canvasModel.cursorEmbeded) { + if (!ffi.canvasModel.cursorEmbedded) { menu.add(MenuEntryDivider()); menu.add(() { final state = ShowRemoteCursorState.find(key); diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart index 57886b2f2..e8361a652 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; import 'package:provider/provider.dart'; @@ -8,7 +9,9 @@ import 'package:provider/provider.dart'; class DesktopRemoteScreen extends StatelessWidget { final Map params; - const DesktopRemoteScreen({Key? key, required this.params}) : super(key: key); + DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) { + bind.mainStartGrabKeyboard(); + } @override Widget build(BuildContext context) { diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart new file mode 100644 index 000000000..cfbdb0c4e --- /dev/null +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -0,0 +1,224 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; + +import '../../common.dart'; + +typedef KBChoosedCallback = Future Function(String); + +const double _kImageMarginVertical = 6.0; +const double _kImageMarginHorizontal = 10.0; +const double _kImageBoarderWidth = 4.0; +const double _kImagePaddingWidth = 4.0; +const Color _kImageBorderColor = Color.fromARGB(125, 202, 247, 2); +const double _kBorderRadius = 6.0; +const String _kKBLayoutTypeISO = 'ISO'; +const String _kKBLayoutTypeNotISO = 'Not ISO'; + +const _kKBLayoutImageMap = { + _kKBLayoutTypeISO: 'kb_layout_iso', + _kKBLayoutTypeNotISO: 'kb_layout_not_iso', +}; + +class _KBImage extends StatelessWidget { + final String kbLayoutType; + final double imageWidth; + final RxString choosedType; + const _KBImage({ + Key? key, + required this.kbLayoutType, + required this.imageWidth, + required this.choosedType, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(_kBorderRadius), + border: Border.all( + color: choosedType.value == kbLayoutType + ? _kImageBorderColor + : Colors.transparent, + width: _kImageBoarderWidth, + ), + ), + margin: EdgeInsets.symmetric( + horizontal: _kImageMarginHorizontal, + vertical: _kImageMarginVertical, + ), + padding: EdgeInsets.all(_kImagePaddingWidth), + child: SvgPicture.asset( + 'assets/${_kKBLayoutImageMap[kbLayoutType] ?? ""}.svg', + width: imageWidth - + _kImageMarginHorizontal * 2 - + _kImagePaddingWidth * 2 - + _kImageBoarderWidth * 2, + ), + ); + }); + } +} + +class _KBChooser extends StatelessWidget { + final String kbLayoutType; + final double imageWidth; + final RxString choosedType; + final KBChoosedCallback cb; + const _KBChooser({ + Key? key, + required this.kbLayoutType, + required this.imageWidth, + required this.choosedType, + required this.cb, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + onChanged(String? v) async { + if (v != null) { + if (await cb(v)) { + choosedType.value = v; + } + } + } + + return Column( + children: [ + TextButton( + onPressed: () { + onChanged(kbLayoutType); + }, + child: _KBImage( + kbLayoutType: kbLayoutType, + imageWidth: imageWidth, + choosedType: choosedType, + ), + style: TextButton.styleFrom(padding: EdgeInsets.zero), + ), + TextButton( + child: Row( + children: [ + Obx(() => Radio( + splashRadius: 0, + value: kbLayoutType, + groupValue: choosedType.value, + onChanged: onChanged, + )), + Text(kbLayoutType), + ], + ), + onPressed: () { + onChanged(kbLayoutType); + }, + ), + ], + ); + } +} + +class KBLayoutTypeChooser extends StatelessWidget { + final RxString choosedType; + final double width; + final double height; + final double dividerWidth; + final KBChoosedCallback cb; + KBLayoutTypeChooser({ + Key? key, + required this.choosedType, + required this.width, + required this.height, + required this.dividerWidth, + required this.cb, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final imageWidth = width / 2 - dividerWidth; + return SizedBox( + width: width, + height: height, + child: Center( + child: Row( + children: [ + _KBChooser( + kbLayoutType: _kKBLayoutTypeISO, + imageWidth: imageWidth, + choosedType: choosedType, + cb: cb, + ), + VerticalDivider( + width: dividerWidth * 2, + ), + _KBChooser( + kbLayoutType: _kKBLayoutTypeNotISO, + imageWidth: imageWidth, + choosedType: choosedType, + cb: cb, + ), + ], + ), + ), + ); + } +} + +RxString KBLayoutType = ''.obs; + +String getLocalPlatformForKBLayoutType(String peerPlatform) { + String localPlatform = ''; + if (peerPlatform != 'Mac OS') { + return localPlatform; + } + + if (Platform.isWindows) { + localPlatform = 'Windows'; + } else if (Platform.isLinux) { + localPlatform = 'Linux'; + } + // to-do: web desktop support ? + return localPlatform; +} + +showKBLayoutTypeChooserIfNeeded( + String peerPlatform, + OverlayDialogManager dialogManager, +) async { + final localPlatform = getLocalPlatformForKBLayoutType(peerPlatform); + if (localPlatform == '') { + return; + } + KBLayoutType.value = bind.getLocalKbLayoutType(); + if (KBLayoutType.value == _kKBLayoutTypeISO || + KBLayoutType.value == _kKBLayoutTypeNotISO) { + return; + } + showKBLayoutTypeChooser(localPlatform, dialogManager); +} + +showKBLayoutTypeChooser( + String localPlatform, + OverlayDialogManager dialogManager, +) { + dialogManager.show((setState, close) { + return CustomAlertDialog( + title: + Text('${translate('Select local keyboard type')} ($localPlatform)'), + content: KBLayoutTypeChooser( + choosedType: KBLayoutType, + width: 360, + height: 200, + dividerWidth: 4.0, + cb: (String v) async { + await bind.setLocalKbLayoutType(kbLayoutType: v); + KBLayoutType.value = bind.getLocalKbLayoutType(); + return v == KBLayoutType.value; + }), + actions: [msgBoxButton(translate('Close'), close)], + onCancel: close, + ); + }); +} diff --git a/flutter/lib/desktop/widgets/list_search_action_listener.dart b/flutter/lib/desktop/widgets/list_search_action_listener.dart new file mode 100644 index 000000000..9598c3400 --- /dev/null +++ b/flutter/lib/desktop/widgets/list_search_action_listener.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; + +class ListSearchActionListener extends StatelessWidget { + final FocusNode node; + final TimeoutStringBuffer buffer; + final Widget child; + final Function(String) onNext; + final Function(String) onSearch; + + const ListSearchActionListener( + {super.key, + required this.node, + required this.buffer, + required this.child, + required this.onNext, + required this.onSearch}); + + @mustCallSuper + @override + Widget build(BuildContext context) { + return KeyboardListener( + autofocus: true, + onKeyEvent: (kv) { + final ch = kv.character; + if (ch == null) { + return; + } + final action = buffer.input(ch); + switch (action) { + case ListSearchAction.search: + onSearch(buffer.buffer); + break; + case ListSearchAction.next: + onNext(buffer.buffer); + break; + } + }, + focusNode: node, + child: child); + } +} + +enum ListSearchAction { search, next } + +class TimeoutStringBuffer { + var _buffer = ""; + late DateTime _duration; + + static int timeoutMilliSec = 1500; + + String get buffer => _buffer; + + TimeoutStringBuffer() { + _duration = DateTime.now(); + } + + ListSearchAction input(String ch) { + final curr = DateTime.now(); + try { + if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) { + _buffer = ch; + return ListSearchAction.search; + } else { + if (ch == _buffer) { + return ListSearchAction.next; + } else { + _buffer += ch; + return ListSearchAction.search; + } + } + } finally { + _duration = curr; + } + } +} diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart deleted file mode 100644 index 053653ab3..000000000 --- a/flutter/lib/desktop/widgets/login.dart +++ /dev/null @@ -1,521 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:flutter_hbb/models/platform_model.dart'; -import 'package:get/get.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import '../../common.dart'; - -final kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0); - -class _IconOP extends StatelessWidget { - final String icon; - final double iconWidth; - const _IconOP({Key? key, required this.icon, required this.iconWidth}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 4.0), - child: SvgPicture.asset( - 'assets/$icon.svg', - width: iconWidth, - ), - ); - } -} - -class ButtonOP extends StatelessWidget { - final String op; - final RxString curOP; - final double iconWidth; - final Color primaryColor; - final double height; - final Function() onTap; - - const ButtonOP({ - Key? key, - required this.op, - required this.curOP, - required this.iconWidth, - required this.primaryColor, - required this.height, - required this.onTap, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Row(children: [ - Expanded( - child: Container( - height: height, - padding: kMidButtonPadding, - child: Obx(() => ElevatedButton( - style: ElevatedButton.styleFrom( - primary: curOP.value.isEmpty || curOP.value == op - ? primaryColor - : Colors.grey, - ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), - onPressed: - curOP.value.isEmpty || curOP.value == op ? onTap : null, - child: Stack(children: [ - Center(child: Text('${translate("Continue with")} $op')), - Align( - alignment: Alignment.centerLeft, - child: SizedBox( - width: 120, - child: _IconOP( - icon: op, - iconWidth: iconWidth, - )), - ), - ]), - )), - ), - ) - ]); - } -} - -class ConfigOP { - final String op; - final double iconWidth; - ConfigOP({required this.op, required this.iconWidth}); -} - -class WidgetOP extends StatefulWidget { - final ConfigOP config; - final RxString curOP; - final Function(String) cbLogin; - const WidgetOP({ - Key? key, - required this.config, - required this.curOP, - required this.cbLogin, - }) : super(key: key); - - @override - State createState() { - return _WidgetOPState(); - } -} - -class _WidgetOPState extends State { - Timer? _updateTimer; - String _stateMsg = ''; - String _FailedMsg = ''; - String _url = ''; - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - _updateTimer?.cancel(); - } - - _beginQueryState() { - _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { - _updateState(); - }); - } - - _updateState() { - bind.mainAccountAuthResult().then((result) { - if (result.isEmpty) { - return; - } - final resultMap = jsonDecode(result); - if (resultMap == null) { - return; - } - final String stateMsg = resultMap['state_msg']; - String failedMsg = resultMap['failed_msg']; - final String? url = resultMap['url']; - final authBody = resultMap['auth_body']; - if (_stateMsg != stateMsg || _FailedMsg != failedMsg) { - if (_url.isEmpty && url != null && url.isNotEmpty) { - launchUrl(Uri.parse(url)); - _url = url; - } - if (authBody != null) { - _updateTimer?.cancel(); - final String username = authBody['user']['name']; - widget.curOP.value = ''; - widget.cbLogin(username); - } - - setState(() { - _stateMsg = stateMsg; - _FailedMsg = failedMsg; - if (failedMsg.isNotEmpty) { - widget.curOP.value = ''; - _updateTimer?.cancel(); - } - }); - } - }); - } - - _resetState() { - _stateMsg = ''; - _FailedMsg = ''; - _url = ''; - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - ButtonOP( - op: widget.config.op, - curOP: widget.curOP, - iconWidth: widget.config.iconWidth, - primaryColor: str2color(widget.config.op, 0x7f), - height: 36, - onTap: () async { - _resetState(); - widget.curOP.value = widget.config.op; - await bind.mainAccountAuth(op: widget.config.op); - _beginQueryState(); - }, - ), - Obx(() { - if (widget.curOP.isNotEmpty && - widget.curOP.value != widget.config.op) { - _FailedMsg = ''; - } - return Offstage( - offstage: - _FailedMsg.isEmpty && widget.curOP.value != widget.config.op, - child: Row( - children: [ - Text( - _stateMsg, - style: TextStyle(fontSize: 12), - ), - SizedBox(width: 8), - Text( - _FailedMsg, - style: TextStyle( - fontSize: 14, - color: Colors.red, - ), - ), - ], - )); - }), - Obx( - () => Offstage( - offstage: widget.curOP.value != widget.config.op, - child: const SizedBox( - height: 5.0, - ), - ), - ), - Obx( - () => Offstage( - offstage: widget.curOP.value != widget.config.op, - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: 20), - child: ElevatedButton( - onPressed: () { - widget.curOP.value = ''; - _updateTimer?.cancel(); - _resetState(); - bind.mainAccountAuthCancel(); - }, - child: Text( - translate('Cancel'), - style: TextStyle(fontSize: 15), - ), - ), - ), - ), - ), - ], - ); - } -} - -class LoginWidgetOP extends StatelessWidget { - final List ops; - final RxString curOP; - final Function(String) cbLogin; - - LoginWidgetOP({ - Key? key, - required this.ops, - required this.curOP, - required this.cbLogin, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - var children = ops - .map((op) => [ - WidgetOP( - config: op, - curOP: curOP, - cbLogin: cbLogin, - ), - const Divider( - indent: 5, - endIndent: 5, - ) - ]) - .expand((i) => i) - .toList(); - if (children.isNotEmpty) { - children.removeLast(); - } - return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: children, - )); - } -} - -class LoginWidgetUserPass extends StatelessWidget { - final String username; - final String pass; - final String usernameMsg; - final String passMsg; - final bool isInProgress; - final RxString curOP; - final Function(String, String) onLogin; - const LoginWidgetUserPass({ - Key? key, - required this.username, - required this.pass, - required this.usernameMsg, - required this.passMsg, - required this.isInProgress, - required this.curOP, - required this.onLogin, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - var userController = TextEditingController(text: username); - var pwdController = TextEditingController(text: pass); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - Container( - padding: kMidButtonPadding, - child: Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - '${translate("Username")}:', - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: usernameMsg.isNotEmpty ? usernameMsg : null), - controller: userController, - focusNode: FocusNode()..requestFocus(), - ), - ), - ], - ), - ), - const SizedBox( - height: 8.0, - ), - Container( - padding: kMidButtonPadding, - child: Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Password")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: passMsg.isNotEmpty ? passMsg : null), - controller: pwdController, - ), - ), - ], - ), - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()), - const SizedBox( - height: 12.0, - ), - Row(children: [ - Expanded( - child: Container( - height: 38, - padding: kMidButtonPadding, - child: Obx(() => ElevatedButton( - style: curOP.value.isEmpty || curOP.value == 'rustdesk' - ? null - : ElevatedButton.styleFrom( - primary: Colors.grey, - ), - child: Text( - translate('Login'), - style: TextStyle(fontSize: 16), - ), - onPressed: curOP.value.isEmpty || curOP.value == 'rustdesk' - ? () { - onLogin(userController.text, pwdController.text); - } - : null, - )), - ), - ), - ]), - ], - ); - } -} - -/// common login dialog for desktop -/// call this directly -Future loginDialog() async { - String username = ''; - var usernameMsg = ''; - String pass = ''; - var passMsg = ''; - var isInProgress = false; - var completer = Completer(); - final RxString curOP = ''.obs; - - gFFI.dialogManager.show((setState, close) { - cancel() { - isInProgress = false; - completer.complete(false); - close(); - } - - onLogin(String username0, String pass0) async { - setState(() { - usernameMsg = ''; - passMsg = ''; - isInProgress = true; - }); - cancel() { - curOP.value = ''; - if (isInProgress) { - setState(() { - isInProgress = false; - }); - } - } - - curOP.value = 'rustdesk'; - username = username0; - pass = pass0; - if (username.isEmpty) { - usernameMsg = translate('Username missed'); - cancel(); - return; - } - if (pass.isEmpty) { - passMsg = translate('Password missed'); - cancel(); - return; - } - try { - final resp = await gFFI.userModel.login(username, pass); - if (resp.containsKey('error')) { - passMsg = resp['error']; - cancel(); - return; - } - // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, - // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} - debugPrint('$resp'); - completer.complete(true); - } catch (err) { - debugPrintStack(label: err.toString()); - cancel(); - return; - } - close(); - } - - return CustomAlertDialog( - title: Text(translate('Login')), - content: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - LoginWidgetUserPass( - username: username, - pass: pass, - usernameMsg: usernameMsg, - passMsg: passMsg, - isInProgress: isInProgress, - curOP: curOP, - onLogin: onLogin, - ), - const SizedBox( - height: 8.0, - ), - Center( - child: Text( - translate('or'), - style: TextStyle(fontSize: 16), - )), - const SizedBox( - height: 8.0, - ), - LoginWidgetOP( - ops: [ - ConfigOP(op: 'Github', iconWidth: 20), - ConfigOP(op: 'Google', iconWidth: 20), - ConfigOP(op: 'Okta', iconWidth: 38), - ], - curOP: curOP, - cbLogin: (String username) { - gFFI.userModel.userName.value = username; - completer.complete(true); - close(); - }, - ), - ], - ), - ), - actions: [msgBoxButton(translate('Close'), cancel)], - onCancel: cancel, - ); - }); - return completer.future; -} diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 20ab31ed9..0cbdad929 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -118,6 +118,15 @@ abstract class MenuEntryBase { this.enabled, }); List> build(BuildContext context, MenuConfig conf); + + enabledStyle(BuildContext context) => TextStyle( + color: Theme.of(context).textTheme.titleLarge?.color, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); + disabledStyle() => TextStyle( + color: Colors.grey, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); } class MenuEntryDivider extends MenuEntryBase { @@ -189,54 +198,76 @@ class MenuEntryRadios extends MenuEntryBase { mod_menu.PopupMenuEntry _buildMenuItem( BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) { + Widget getTextChild() { + final enabledTextChild = Text( + opt.text, + style: enabledStyle(context), + ); + final disabledTextChild = Text( + opt.text, + style: disabledStyle(), + ); + if (opt.enabled == null) { + return enabledTextChild; + } else { + return Obx( + () => opt.enabled!.isTrue ? enabledTextChild : disabledTextChild); + } + } + + final child = Container( + padding: padding, + alignment: AlignmentDirectional.centerStart, + constraints: + BoxConstraints(minHeight: conf.height, maxHeight: conf.height), + child: Row( + children: [ + getTextChild(), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: MenuConfig.iconScale, + child: Obx(() => opt.value == curOption.value + ? IconButton( + padding: + const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), + hoverColor: Colors.transparent, + focusColor: Colors.transparent, + onPressed: () {}, + icon: Icon( + Icons.check, + color: (opt.enabled ?? true.obs).isTrue + ? conf.commonColor + : Colors.grey, + )) + : const SizedBox.shrink()), + ))), + ], + ), + ); + onPressed() { + if (opt.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(opt.value); + } + return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, child: Container( - width: conf.boxWidth, - child: TextButton( - child: Container( - padding: padding, - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints( - minHeight: conf.height, maxHeight: conf.height), - child: Row( - children: [ - Text( - opt.text, - style: TextStyle( - color: Theme.of(context).textTheme.titleLarge?.color, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: MenuConfig.iconScale, - child: Obx(() => opt.value == curOption.value - ? IconButton( - padding: const EdgeInsets.fromLTRB( - 8.0, 0.0, 8.0, 0.0), - hoverColor: Colors.transparent, - focusColor: Colors.transparent, - onPressed: () {}, - icon: Icon( - Icons.check, - color: conf.commonColor, - )) - : const SizedBox.shrink()), - ))), - ], - ), - ), - onPressed: () { - if (opt.dismissOnClicked && Navigator.canPop(context)) { - Navigator.pop(context); - } - setOption(opt.value); - }, - )), + width: conf.boxWidth, + child: opt.enabled == null + ? TextButton( + child: child, + onPressed: onPressed, + ) + : Obx(() => TextButton( + child: child, + onPressed: opt.enabled!.isTrue ? onPressed : null, + )), + ), ); } @@ -567,12 +598,9 @@ class MenuEntrySubMenu extends MenuEntryBase { const SizedBox(width: MenuConfig.midPadding), Obx(() => Text( text, - style: TextStyle( - color: super.enabled!.value - ? Theme.of(context).textTheme.titleLarge?.color - : Colors.grey, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), + style: super.enabled!.value + ? enabledStyle(context) + : disabledStyle(), )), Expanded( child: Align( @@ -605,14 +633,6 @@ class MenuEntryButton extends MenuEntryBase { ); Widget _buildChild(BuildContext context, MenuConfig conf) { - final enabledStyle = TextStyle( - color: Theme.of(context).textTheme.titleLarge?.color, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal); - const disabledStyle = TextStyle( - color: Colors.grey, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal); super.enabled ??= true.obs; return Obx(() => Container( width: conf.boxWidth, @@ -631,7 +651,7 @@ class MenuEntryButton extends MenuEntryBase { constraints: BoxConstraints(minHeight: conf.height, maxHeight: conf.height), child: childBuilder( - super.enabled!.value ? enabledStyle : disabledStyle), + super.enabled!.value ? enabledStyle(context) : disabledStyle()), ), ))); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 2ae5b96c4..b69c73091 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -22,6 +22,7 @@ import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import './popup_menu.dart'; import './material_mod_popup_menu.dart' as mod_menu; +import './kb_layout_type_chooser.dart'; class MenubarState { final kStoreKey = 'remoteMenubarState'; @@ -171,6 +172,8 @@ class _RemoteMenubarState extends State { @override Widget build(BuildContext context) { + // No need to use future builder here. + _updateScreen(); return Align( alignment: Alignment.topCenter, child: Obx(() => show.value @@ -362,8 +365,6 @@ class _RemoteMenubarState extends State { RxInt display = CurrentDisplayState.find(widget.id); if (display.value != i) { bind.sessionSwitchDisplay(id: widget.id, value: i); - pi.currentDisplay = i; - display.value = i; } }, ) @@ -569,7 +570,8 @@ class _RemoteMenubarState extends State { ), ]); // {handler.get_audit_server() &&
  • {translate('Note')}
  • } - final auditServer = bind.sessionGetAuditServerSync(id: widget.id); + final auditServer = + bind.sessionGetAuditServerSync(id: widget.id, typ: "conn"); if (auditServer.isNotEmpty) { displayMenu.add( MenuEntryButton( @@ -697,12 +699,12 @@ class _RemoteMenubarState extends State { if (_screen == null) { return false; } - double scale = _screen!.scaleFactor; - double selfWidth = _screen!.frame.width; - double selfHeight = _screen!.frame.height; + final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor; + double selfWidth = _screen!.visibleFrame.width; + double selfHeight = _screen!.visibleFrame.height; if (isFullscreen) { - selfWidth = _screen!.visibleFrame.width; - selfHeight = _screen!.visibleFrame.height; + selfWidth = _screen!.frame.width; + selfHeight = _screen!.frame.height; } final canvasModel = widget.ffi.canvasModel; @@ -827,7 +829,7 @@ class _RemoteMenubarState extends State { qualityInitValue = qualityMaxValue; } final RxDouble qualitySliderValue = RxDouble(qualityInitValue); - final debouncerQuanlity = Debouncer( + final debouncerQuality = Debouncer( Duration(milliseconds: 1000), onChanged: (double v) { setCustomValues(quality: v); @@ -843,7 +845,7 @@ class _RemoteMenubarState extends State { divisions: 90, onChanged: (double value) { qualitySliderValue.value = value; - debouncerQuanlity.value = value; + debouncerQuality.value = value; }, ), SizedBox( @@ -934,11 +936,13 @@ class _RemoteMenubarState extends State { text: translate('ScrollAuto'), value: kRemoteScrollStyleAuto, dismissOnClicked: true, + enabled: widget.ffi.canvasModel.imageOverflow, ), MenuEntryRadioOption( text: translate('Scrollbar'), value: kRemoteScrollStyleBar, dismissOnClicked: true, + enabled: widget.ffi.canvasModel.imageOverflow, ), ], curOptionGetter: () async => @@ -984,15 +988,17 @@ class _RemoteMenubarState extends State { wndRect.bottom - wndRect.top - mediaSize.height * scale; final canvasModel = widget.ffi.canvasModel; - final width = (canvasModel.getDisplayWidth() + - canvasModel.windowBorderWidth * 2) * - scale + - magicWidth; - final height = (canvasModel.getDisplayHeight() + - canvasModel.tabBarHeight + - canvasModel.windowBorderWidth * 2) * - scale + - magicHeight; + final width = + (canvasModel.getDisplayWidth() * canvasModel.scale + + canvasModel.windowBorderWidth * 2) * + scale + + magicWidth; + final height = + (canvasModel.getDisplayHeight() * canvasModel.scale + + canvasModel.tabBarHeight + + canvasModel.windowBorderWidth * 2) * + scale + + magicHeight; double left = wndRect.left + (wndRect.width - width) / 2; double top = wndRect.top + (wndRect.height - height) / 2; @@ -1032,7 +1038,9 @@ class _RemoteMenubarState extends State { final h265 = codecsJson['h265'] ?? false; codecs.add(h264); codecs.add(h265); - } finally {} + } catch (e) { + debugPrint("Show Codec Preference err=$e"); + } if (codecs.length == 2 && (codecs[0] || codecs[1])) { displayMenu.add(MenuEntryRadios( text: translate('Codec Preference'), @@ -1082,7 +1090,7 @@ class _RemoteMenubarState extends State { } /// Show remote cursor - if (!widget.ffi.canvasModel.cursorEmbeded) { + if (!widget.ffi.canvasModel.cursorEmbedded) { displayMenu.add(() { final state = ShowRemoteCursorState.find(widget.id); return MenuEntrySwitch2( @@ -1182,22 +1190,70 @@ class _RemoteMenubarState extends State { } List> _getKeyboardMenu() { - final keyboardMenu = [ + final List> keyboardMenu = [ MenuEntryRadios( text: translate('Ratio'), optionsGetter: () => [ MenuEntryRadioOption(text: translate('Legacy mode'), value: 'legacy'), MenuEntryRadioOption(text: translate('Map mode'), value: 'map'), ], - curOptionGetter: () async => - await bind.sessionGetKeyboardName(id: widget.id), + curOptionGetter: () async { + return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; + }, optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetKeyboardMode( - id: widget.id, keyboardMode: newValue); + await bind.sessionSetKeyboardMode(id: widget.id, value: newValue); }, ) ]; - + final localPlatform = + getLocalPlatformForKBLayoutType(widget.ffi.ffiModel.pi.platform); + if (localPlatform != '') { + keyboardMenu.add(MenuEntryDivider()); + keyboardMenu.add( + MenuEntryButton( + childBuilder: (TextStyle? style) => Container( + alignment: AlignmentDirectional.center, + height: _MenubarTheme.height, + child: Row( + children: [ + Obx(() => RichText( + text: TextSpan( + text: '${translate('Local keyboard type')}: ', + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: KBLayoutType.value, + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + )), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.settings), + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + showKBLayoutTypeChooser( + localPlatform, widget.ffi.dialogManager); + }, + ), + ), + )) + ], + )), + proc: () {}, + padding: EdgeInsets.zero, + dismissOnClicked: false, + ), + ); + } return keyboardMenu; } @@ -1357,10 +1413,10 @@ class _DraggableShowHide extends StatefulWidget { }) : super(key: key); @override - State<_DraggableShowHide> createState() => __DraggableShowHideState(); + State<_DraggableShowHide> createState() => _DraggableShowHideState(); } -class __DraggableShowHideState extends State<_DraggableShowHide> { +class _DraggableShowHideState extends State<_DraggableShowHide> { Offset position = Offset.zero; Size size = Size.zero; @@ -1369,7 +1425,8 @@ class __DraggableShowHideState extends State<_DraggableShowHide> { axis: Axis.horizontal, child: Icon( Icons.drag_indicator, - size: 15, + size: 20, + color: Colors.grey, ), feedback: widget, onDragStarted: (() { @@ -1412,7 +1469,7 @@ class __DraggableShowHideState extends State<_DraggableShowHide> { }), child: Obx((() => Icon( widget.show.isTrue ? Icons.expand_less : Icons.expand_more, - size: 15, + size: 20, ))), ), ], @@ -1425,7 +1482,7 @@ class __DraggableShowHideState extends State<_DraggableShowHide> { border: Border.all(color: MyTheme.border), ), child: SizedBox( - height: 15, + height: 20, child: child, ), ), diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 436011cb5..91ce6cce6 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -331,6 +331,7 @@ class DesktopTab extends StatelessWidget { return _buildBlock( child: Obx(() => PageView( controller: state.value.pageController, + physics: NeverScrollableScrollPhysics(), children: state.value.tabs .map((tab) => tab.page) .toList(growable: false)))); @@ -526,13 +527,19 @@ class WindowActionPanelState extends State void onWindowClose() async { // hide window on close if (widget.isMainWindow) { + await rustDeskWinManager.unregisterActiveWindow(0); + // `hide` must be placed after unregisterActiveWindow, because once all windows are hidden, + // flutter closes the application on macOS. We should ensure the post-run logic has ran successfully. + // e.g.: saving window position. await windowManager.hide(); - rustDeskWinManager.unregisterActiveWindow(0); } else { - widget.onClose?.call(); + // it's safe to hide the subwindow await WindowController.fromWindowId(windowId!).hide(); - rustDeskWinManager - .call(WindowType.Main, kWindowEventHide, {"id": windowId!}); + await Future.wait([ + rustDeskWinManager + .call(WindowType.Main, kWindowEventHide, {"id": windowId!}), + widget.onClose?.call() ?? Future.microtask(() => null) + ]); } super.onWindowClose(); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 2015c02b2..6fd205a22 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -117,6 +117,7 @@ void runMainApp(bool startService) async { // await windowManager.ensureInitialized(); gFFI.serverModel.startService(); } + gFFI.userModel.refreshCurrentUser(); runApp(App()); // restore the location of the main window before window hide or show await restoreWindowPosition(WindowType.Main); @@ -195,6 +196,8 @@ void runMultiWindow( // no such appType exit(0); } + // show window from hidden status + WindowController.fromWindowId(windowId!).show(); } void runConnectionManagerScreen(bool hide) async { diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 957910324..6fce887bf 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -7,9 +7,8 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; -import '../../common/widgets/address_book.dart'; +import '../../common/widgets/login.dart'; import '../../common/widgets/peer_tab_page.dart'; -import '../../common/widgets/peers_view.dart'; import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; @@ -258,7 +257,7 @@ class _WebMenuState extends State { } if (value == 'login') { if (gFFI.userModel.userName.value.isEmpty) { - showLogin(gFFI.dialogManager); + loginDialog(); } else { gFFI.userModel.logOut(); } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index fcfb8ad60..97ce6268d 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -518,7 +518,7 @@ class _RemotePageState extends State { ), ), ]; - if (!gFFI.canvasModel.cursorEmbeded) { + if (!gFFI.canvasModel.cursorEmbedded) { paints.add(CursorPaint()); } return paints; @@ -527,7 +527,7 @@ class _RemotePageState extends State { Widget getBodyForDesktopWithListener(bool keyboard) { var paints = [ImagePaint()]; - if (!gFFI.canvasModel.cursorEmbeded) { + if (!gFFI.canvasModel.cursorEmbedded) { final cursor = bind.sessionGetToggleOptionSync( id: widget.id, arg: 'show-remote-cursor'); if (keyboard || cursor) { @@ -692,10 +692,11 @@ class _RemotePageState extends State { } void changePhysicalKeyboardInputMode() async { - var current = await bind.sessionGetKeyboardName(id: widget.id); + var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; gFFI.dialogManager.show((setState, close) { void setMode(String? v) async { - await bind.sessionSetKeyboardMode(id: widget.id, keyboardMode: v ?? ''); + await bind.sessionPeerOption( + id: widget.id, name: "keyboard-mode", value: v ?? ""); setState(() => current = v ?? ''); Future.delayed(Duration(milliseconds: 300), close); } @@ -977,7 +978,9 @@ void showOptions( final h265 = codecsJson['h265'] ?? false; codecs.add(h264); codecs.add(h265); - } finally {} + } catch (e) { + debugPrint("Show Codec Preference err=$e"); + } } dialogManager.show((setState, close) { @@ -1055,7 +1058,7 @@ void showOptions( final toggles = [ getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'), ]; - if (!gFFI.canvasModel.cursorEmbeded) { + if (!gFFI.canvasModel.cursorEmbedded) { toggles.insert(0, getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor')); } diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 3bd381d92..8778d78f7 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -9,11 +8,11 @@ import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:zxing2/qrcode.dart'; import '../../common.dart'; -import '../../models/platform_model.dart'; +import '../widgets/dialog.dart'; class ScanPage extends StatefulWidget { @override - _ScanPageState createState() => _ScanPageState(); + State createState() => _ScanPageState(); } class _ScanPageState extends State { @@ -42,9 +41,9 @@ class _ScanPageState extends State { icon: Icon(Icons.image_search), iconSize: 32.0, onPressed: () async { - final ImagePicker _picker = ImagePicker(); + final ImagePicker picker = ImagePicker(); final XFile? file = - await _picker.pickImage(source: ImageSource.gallery); + await picker.pickImage(source: ImageSource.gallery); if (file != null) { var image = img.decodeNamedImage( File(file.path).readAsBytesSync(), file.path)!; @@ -139,155 +138,12 @@ class _ScanPageState extends State { return; } try { - Map values = json.decode(data.substring(7)); - var host = values['host'] != null ? values['host'] as String : ''; - var key = values['key'] != null ? values['key'] as String : ''; - var api = values['api'] != null ? values['api'] as String : ''; + final sc = ServerConfig.decode(data.substring(7)); Timer(Duration(milliseconds: 60), () { - showServerSettingsWithValue(host, '', key, api, gFFI.dialogManager); + showServerSettingsWithValue(sc, gFFI.dialogManager); }); } catch (e) { showToast('Invalid QR code'); } } } - -void showServerSettingsWithValue(String id, String relay, String key, - String api, OverlayDialogManager dialogManager) async { - Map oldOptions = jsonDecode(await bind.mainGetOptions()); - String id0 = oldOptions['custom-rendezvous-server'] ?? ""; - String relay0 = oldOptions['relay-server'] ?? ""; - String api0 = oldOptions['api-server'] ?? ""; - String key0 = oldOptions['key'] ?? ""; - var isInProgress = false; - final idController = TextEditingController(text: id); - final relayController = TextEditingController(text: relay); - final apiController = TextEditingController(text: api); - - String? idServerMsg; - String? relayServerMsg; - String? apiServerMsg; - - dialogManager.show((setState, close) { - Future validate() async { - if (idController.text != id) { - final res = await validateAsync(idController.text); - setState(() => idServerMsg = res); - if (idServerMsg != null) return false; - id = idController.text; - } - if (relayController.text != relay) { - relayServerMsg = await validateAsync(relayController.text); - if (relayServerMsg != null) return false; - relay = relayController.text; - } - if (apiController.text != relay) { - apiServerMsg = await validateAsync(apiController.text); - if (apiServerMsg != null) return false; - api = apiController.text; - } - return true; - } - - return CustomAlertDialog( - title: Text(translate('ID/Relay Server')), - content: Form( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - controller: idController, - decoration: InputDecoration( - labelText: translate('ID Server'), - errorText: idServerMsg), - ) - ] + - (isAndroid - ? [ - TextFormField( - controller: relayController, - decoration: InputDecoration( - labelText: translate('Relay Server'), - errorText: relayServerMsg), - ) - ] - : []) + - [ - TextFormField( - controller: apiController, - decoration: InputDecoration( - labelText: translate('API Server'), - ), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (v) { - if (v != null && v.length > 0) { - if (!(v.startsWith('http://') || - v.startsWith("https://"))) { - return translate("invalid_http"); - } - } - return apiServerMsg; - }, - ), - TextFormField( - initialValue: key, - decoration: InputDecoration( - labelText: 'Key', - ), - onChanged: (String? value) { - if (value != null) key = value.trim(); - }, - ), - Offstage( - offstage: !isInProgress, - child: LinearProgressIndicator()) - ])), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: () async { - setState(() { - idServerMsg = null; - relayServerMsg = null; - apiServerMsg = null; - isInProgress = true; - }); - if (await validate()) { - if (id != id0) { - bind.mainSetOption(key: "custom-rendezvous-server", value: id); - } - if (relay != relay0) { - bind.mainSetOption(key: "relay-server", value: relay); - } - if (key != key0) bind.mainSetOption(key: "key", value: key); - if (api != api0) { - bind.mainSetOption(key: "api-server", value: api); - } - close(); - } - setState(() { - isInProgress = false; - }); - }, - child: Text(translate('OK')), - ), - ], - ); - }); -} - -Future validateAsync(String value) async { - value = value.trim(); - if (value.isEmpty) { - return null; - } - final res = await bind.mainTestIfValidServer(server: value); - return res.isEmpty ? null : res; -} diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 38ad18f14..abccdf683 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:provider/provider.dart'; @@ -107,12 +109,23 @@ class ServerPage extends StatefulWidget implements PageShape { } class _ServerPageState extends State { + Timer? _updateTimer; + @override void initState() { super.initState(); + _updateTimer = periodic_immediate(const Duration(seconds: 3), () async { + await gFFI.serverModel.fetchID(); + }); gFFI.serverModel.checkAndroidPermission(); } + @override + void dispose() { + _updateTimer?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { checkService(); diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 269439b1d..b14f3ee65 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; import '../../common/widgets/dialog.dart'; +import '../../common/widgets/login.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; @@ -300,7 +301,7 @@ class _SettingsState extends State with WidgetsBindingObserver { leading: Icon(Icons.person), onPressed: (context) { if (gFFI.userModel.userName.value.isEmpty) { - showLogin(gFFI.dialogManager); + loginDialog(); } else { gFFI.userModel.logOut(); } @@ -391,17 +392,13 @@ class _SettingsState extends State with WidgetsBindingObserver { void showServerSettings(OverlayDialogManager dialogManager) async { Map options = jsonDecode(await bind.mainGetOptions()); - String id = options['custom-rendezvous-server'] ?? ""; - String relay = options['relay-server'] ?? ""; - String api = options['api-server'] ?? ""; - String key = options['key'] ?? ""; - showServerSettingsWithValue(id, relay, key, api, dialogManager); + showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager); } void showLanguageSettings(OverlayDialogManager dialogManager) async { try { final langs = json.decode(await bind.mainGetLangs()) as List; - var lang = await bind.mainGetLocalOption(key: "lang"); + var lang = bind.mainGetLocalOption(key: "lang"); dialogManager.show((setState, close) { setLang(v) { if (lang != v) { @@ -486,78 +483,6 @@ void showAbout(OverlayDialogManager dialogManager) { }, clickMaskDismiss: true, backDismiss: true); } -void showLogin(OverlayDialogManager dialogManager) { - final passwordController = TextEditingController(); - final nameController = TextEditingController(); - var loading = false; - var error = ''; - dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(translate('Login')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - TextField( - autofocus: true, - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Username'), - ), - controller: nameController, - ), - PasswordWidget(controller: passwordController, autoFocus: false), - ]), - actions: (loading - ? [CircularProgressIndicator()] - : (error != "" - ? [ - Text(translate(error), - style: TextStyle(color: Colors.red)) - ] - : [])) + - [ - TextButton( - style: flatButtonStyle, - onPressed: loading - ? null - : () { - close(); - setState(() { - loading = false; - }); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: loading - ? null - : () async { - final name = nameController.text.trim(); - final pass = passwordController.text.trim(); - if (name != "" && pass != "") { - setState(() { - loading = true; - }); - final resp = await gFFI.userModel.login(name, pass); - setState(() { - loading = false; - }); - if (resp.containsKey('error')) { - error = resp['error']; - return; - } - gFFI.abModel.pullAb(); - } - close(); - }, - child: Text(translate('OK')), - ), - ], - ); - }); -} - class ScanButton extends StatelessWidget { @override Widget build(BuildContext context) { diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 96f96658a..2df80d9fd 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; import '../../common.dart'; @@ -236,6 +237,145 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { ])); } +void showServerSettingsWithValue( + ServerConfig serverConfig, OverlayDialogManager dialogManager) async { + Map oldOptions = jsonDecode(await bind.mainGetOptions()); + final oldCfg = ServerConfig.fromOptions(oldOptions); + + var isInProgress = false; + final idCtrl = TextEditingController(text: serverConfig.idServer); + final relayCtrl = TextEditingController(text: serverConfig.relayServer); + final apiCtrl = TextEditingController(text: serverConfig.apiServer); + final keyCtrl = TextEditingController(text: serverConfig.key); + + String? idServerMsg; + String? relayServerMsg; + String? apiServerMsg; + + dialogManager.show((setState, close) { + Future validate() async { + if (idCtrl.text != oldCfg.idServer) { + final res = await validateAsync(idCtrl.text); + setState(() => idServerMsg = res); + if (idServerMsg != null) return false; + } + if (relayCtrl.text != oldCfg.relayServer) { + relayServerMsg = await validateAsync(relayCtrl.text); + if (relayServerMsg != null) return false; + } + if (apiCtrl.text != oldCfg.apiServer) { + if (apiServerMsg != null) return false; + } + return true; + } + + return CustomAlertDialog( + title: Text(translate('ID/Relay Server')), + content: Form( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: idCtrl, + decoration: InputDecoration( + labelText: translate('ID Server'), + errorText: idServerMsg), + ) + ] + + (isAndroid + ? [ + TextFormField( + controller: relayCtrl, + decoration: InputDecoration( + labelText: translate('Relay Server'), + errorText: relayServerMsg), + ) + ] + : []) + + [ + TextFormField( + controller: apiCtrl, + decoration: InputDecoration( + labelText: translate('API Server'), + ), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (v) { + if (v != null && v.isNotEmpty) { + if (!(v.startsWith('http://') || + v.startsWith("https://"))) { + return translate("invalid_http"); + } + } + return apiServerMsg; + }, + ), + TextFormField( + controller: keyCtrl, + decoration: InputDecoration( + labelText: 'Key', + ), + ), + Offstage( + offstage: !isInProgress, + child: LinearProgressIndicator()) + ])), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () { + close(); + }, + child: Text(translate('Cancel')), + ), + TextButton( + style: flatButtonStyle, + onPressed: () async { + setState(() { + idServerMsg = null; + relayServerMsg = null; + apiServerMsg = null; + isInProgress = true; + }); + if (await validate()) { + if (idCtrl.text != oldCfg.idServer) { + if (oldCfg.idServer.isNotEmpty) { + await gFFI.userModel.logOut(); + } + bind.mainSetOption( + key: "custom-rendezvous-server", value: idCtrl.text); + } + if (relayCtrl.text != oldCfg.relayServer) { + bind.mainSetOption(key: "relay-server", value: relayCtrl.text); + } + if (keyCtrl.text != oldCfg.key) { + bind.mainSetOption(key: "key", value: keyCtrl.text); + } + if (apiCtrl.text != oldCfg.apiServer) { + bind.mainSetOption(key: "api-server", value: apiCtrl.text); + } + close(); + showToast(translate('Successful')); + } + setState(() { + isInProgress = false; + }); + }, + child: Text(translate('OK')), + ), + ], + ); + }); +} + +Future validateAsync(String value) async { + value = value.trim(); + if (value.isEmpty) { + return null; + } + final res = await bind.mainTestIfValidServer(server: value); + return res.isEmpty ? null : res; +} + class PasswordWidget extends StatefulWidget { PasswordWidget({Key? key, required this.controller, this.autoFocus = true}) : super(key: key); @@ -285,7 +425,7 @@ class _PasswordWidgetState extends State { color: Theme.of(context).primaryColorDark, ), onPressed: () { - // Update the state i.e. toogle the state of passwordVisible variable + // Update the state i.e. toggle the state of passwordVisible variable setState(() { _passwordVisible = !_passwordVisible; }); diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index ab5a7cb80..175c8424b 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -21,16 +21,13 @@ class AbModel { AbModel(this.parent); - FFI? get _ffi => parent.target; - Future pullAb() async { - if (_ffi!.userModel.userName.isEmpty) return; + if (gFFI.userModel.userName.isEmpty) return; abLoading.value = true; abError.value = ""; final api = "${await bind.mainGetApiServer()}/api/ab/get"; try { - final resp = - await http.post(Uri.parse(api), headers: await getHttpHeaders()); + final resp = await http.post(Uri.parse(api), headers: getHttpHeaders()); if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); if (json.containsKey('error')) { @@ -63,7 +60,8 @@ class AbModel { return null; } - void reset() { + Future reset() async { + await bind.mainSetLocalOption(key: "selected-tags", value: ''); tags.clear(); peers.clear(); } @@ -103,7 +101,7 @@ class AbModel { Future pushAb() async { abLoading.value = true; final api = "${await bind.mainGetApiServer()}/api/ab"; - var authHeaders = await getHttpHeaders(); + var authHeaders = getHttpHeaders(); authHeaders['Content-Type'] = "application/json"; final peersJsonData = peers.map((e) => e.toJson()).toList(); final body = jsonEncode({ @@ -188,9 +186,4 @@ class AbModel { await pushAb(); } } - - void clear() { - peers.clear(); - tags.clear(); - } } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index a7968f701..c08d2e623 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -213,7 +213,6 @@ class FileModel extends ChangeNotifier { } receiveFileDir(Map evt) { - debugPrint("recv file dir:$evt"); if (evt['is_local'] == "false") { // init remote home, the connection will automatic read remote home when established, try { @@ -237,7 +236,9 @@ class FileModel extends ChangeNotifier { debugPrint("init remote home:${fd.path}"); _currentRemoteDir = fd; } - } finally {} + } catch (e) { + debugPrint("receiveFileDir err=$e"); + } } _fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); notifyListeners(); diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart new file mode 100644 index 000000000..4d9fab0e4 --- /dev/null +++ b/flutter/lib/models/group_model.dart @@ -0,0 +1,140 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/peer_tab_page.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class GroupModel { + final RxBool userLoading = false.obs; + final RxString userLoadError = "".obs; + final RxBool peerLoading = false.obs; //to-do: not used + final RxString peerLoadError = "".obs; + final RxList users = RxList.empty(growable: true); + final RxList peerPayloads = RxList.empty(growable: true); + final RxList peersShow = RxList.empty(growable: true); + WeakReference parent; + + GroupModel(this.parent); + + Future reset() async { + userLoading.value = false; + userLoadError.value = ""; + peerLoading.value = false; + peerLoadError.value = ""; + users.clear(); + peerPayloads.clear(); + peersShow.clear(); + } + + Future pull() async { + await reset(); + if (gFFI.userModel.userName.isEmpty || + (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { + statePeerTab.check(); + return; + } + userLoading.value = true; + userLoadError.value = ""; + final api = "${await bind.mainGetApiServer()}/api/users"; + try { + var uri0 = Uri.parse(api); + final pageSize = 20; + 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(), + if (gFFI.userModel.isAdmin.isFalse) + 'grp': gFFI.userModel.groupName.value, + }); + final resp = await http.get(uri, headers: getHttpHeaders()); + if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { + Map json = jsonDecode(resp.body); + if (json.containsKey('error')) { + throw json['error']; + } else { + if (total == 0) total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final user in data) { + users.add(UserPayload.fromJson(user)); + } + } + } + } + } + } while (current * pageSize < total); + } catch (err) { + debugPrint('$err'); + userLoadError.value = err.toString(); + } finally { + userLoading.value = false; + statePeerTab.check(); + } + } + + Future pullUserPeers(String username) async { + peerPayloads.clear(); + peersShow.clear(); + peerLoading.value = true; + peerLoadError.value = ""; + final api = "${await bind.mainGetApiServer()}/api/peers"; + try { + var uri0 = Uri.parse(api); + final pageSize = 20; + 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(), + 'grp': gFFI.userModel.groupName.value, + 'target_user': username + }); + final resp = await http.get(uri, headers: getHttpHeaders()); + if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { + Map json = jsonDecode(resp.body); + if (json.containsKey('error')) { + throw json['error']; + } else { + if (total == 0) total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final p in data) { + final peer = PeerPayload.fromJson(p); + peerPayloads.add(peer); + peersShow.add(PeerPayload.toPeer(peer)); + } + } + } + } + } + } while (current * pageSize < total); + } catch (err) { + debugPrint('$err'); + peerLoadError.value = err.toString(); + } finally { + peerLoading.value = false; + } + } +} diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index bd1131c7a..0137b784e 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -17,6 +17,10 @@ import './state_model.dart'; /// Mouse button enum. enum MouseButtons { left, right, wheel } +const _kMouseEventDown = 'mousedown'; +const _kMouseEventUp = 'mouseup'; +const _kMouseEventMove = 'mousemove'; + extension ToString on MouseButtons { String get value { switch (this) { @@ -46,7 +50,7 @@ class InputModel { // mouse final isPhysicalMouse = false.obs; - int _lastMouseDownButtons = 0; + int _lastButtons = 0; Offset lastMousePos = Offset.zero; get id => parent.target?.id ?? ""; @@ -54,7 +58,7 @@ class InputModel { InputModel(this.parent); KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { - bind.sessionGetKeyboardName(id: id).then((result) { + bind.sessionGetKeyboardMode(id: id).then((result) { keyboardMode = result.toString(); }); @@ -183,20 +187,42 @@ class InputModel { Map getEvent(PointerEvent evt, String type) { final Map out = {}; - out['type'] = type; out['x'] = evt.position.dx; out['y'] = evt.position.dy; if (alt) out['alt'] = 'true'; if (shift) out['shift'] = 'true'; if (ctrl) out['ctrl'] = 'true'; if (command) out['command'] = 'true'; - out['buttons'] = evt - .buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right) - if (evt.buttons != 0) { - _lastMouseDownButtons = evt.buttons; + + // Check update event type and set buttons to be sent. + int buttons = _lastButtons; + if (type == _kMouseEventMove) { + // flutter may emit move event if one button is pressed and anoter button + // is pressing or releasing. + if (evt.buttons != _lastButtons) { + // For simplicity + // Just consider 3 - 1 ((Left + Right buttons) - Left button) + // Do not consider 2 - 1 (Right button - Left button) + // or 6 - 5 ((Right + Mid buttons) - (Left + Mid buttons)) + // and so on + buttons = evt.buttons - _lastButtons; + if (buttons > 0) { + type = _kMouseEventDown; + } else { + type = _kMouseEventUp; + buttons = -buttons; + } + } } else { - out['buttons'] = _lastMouseDownButtons; + if (evt.buttons != 0) { + buttons = evt.buttons; + } } + _lastButtons = evt.buttons; + + out['buttons'] = buttons; + out['type'] = type; + return out; } @@ -260,7 +286,7 @@ class InputModel { isPhysicalMouse.value = true; } if (isPhysicalMouse.value) { - handleMouse(getEvent(e, 'mousemove')); + handleMouse(getEvent(e, _kMouseEventMove)); } } @@ -325,21 +351,21 @@ class InputModel { } } if (isPhysicalMouse.value) { - handleMouse(getEvent(e, 'mousedown')); + handleMouse(getEvent(e, _kMouseEventDown)); } } void onPointUpImage(PointerUpEvent e) { if (e.kind != ui.PointerDeviceKind.mouse) return; if (isPhysicalMouse.value) { - handleMouse(getEvent(e, 'mouseup')); + handleMouse(getEvent(e, _kMouseEventUp)); } } void onPointMoveImage(PointerMoveEvent e) { if (e.kind != ui.PointerDeviceKind.mouse) return; if (isPhysicalMouse.value) { - handleMouse(getEvent(e, 'mousemove')); + handleMouse(getEvent(e, _kMouseEventMove)); } } @@ -388,13 +414,13 @@ class InputModel { var type = ''; var isMove = false; switch (evt['type']) { - case 'mousedown': + case _kMouseEventDown: type = 'down'; break; - case 'mouseup': + case _kMouseEventUp: type = 'up'; break; - case 'mousemove': + case _kMouseEventMove: isMove = true; break; default: @@ -440,15 +466,21 @@ class InputModel { evt['y'] = '${y.round()}'; var buttons = ''; switch (evt['buttons']) { - case 1: + case kPrimaryMouseButton: buttons = 'left'; break; - case 2: + case kSecondaryMouseButton: buttons = 'right'; break; - case 4: + case kMiddleMouseButton: buttons = 'wheel'; break; + case kBackMouseButton: + buttons = 'back'; + break; + case kForwardMouseButton: + buttons = 'forward'; + break; } evt['buttons'] = buttons; bind.sessionSendMouse(id: id, msg: json.encode(evt)); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 805bcde33..1cdecbd03 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -12,6 +12,7 @@ import 'package:flutter_hbb/generated_bridge.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; +import 'package:flutter_hbb/models/group_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'; @@ -19,8 +20,9 @@ import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:tuple/tuple.dart'; import 'package:image/image.dart' as img2; -import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; +import 'package:flutter_custom_cursor/cursor_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; import '../common.dart'; import '../common/shared_state.dart'; @@ -140,7 +142,7 @@ class FfiModel with ChangeNotifier { setConnectionType( peerId, evt['secure'] == 'true', evt['direct'] == 'true'); } else if (name == 'switch_display') { - handleSwitchDisplay(evt); + handleSwitchDisplay(evt, peerId); } else if (name == 'cursor_data') { await parent.target?.cursorModel.updateCursorData(evt); } else if (name == 'cursor_id') { @@ -213,7 +215,7 @@ class FfiModel with ChangeNotifier { } } - handleSwitchDisplay(Map evt) { + handleSwitchDisplay(Map evt, String peerId) { final oldOrientation = _display.width > _display.height; var old = _pi.currentDisplay; _pi.currentDisplay = int.parse(evt['display']); @@ -221,11 +223,17 @@ class FfiModel with ChangeNotifier { _display.y = double.parse(evt['y']); _display.width = int.parse(evt['width']); _display.height = int.parse(evt['height']); - _display.cursorEmbeded = int.parse(evt['cursor_embeded']) == 1; + _display.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1; if (old != _pi.currentDisplay) { parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); } + try { + CurrentDisplayState.find(peerId).value = _pi.currentDisplay; + } catch (e) { + // + } + // remote is mobile, and orientation changed if ((_display.width > _display.height) != oldOrientation) { gFFI.canvasModel.updateViewStyle(); @@ -331,7 +339,7 @@ class FfiModel with ChangeNotifier { d.y = d0['y'].toDouble(); d.width = d0['width']; d.height = d0['height']; - d.cursorEmbeded = d0['cursor_embeded'] == 1; + d.cursorEmbedded = d0['cursor_embedded'] == 1; _pi.displays.add(d); } if (_pi.currentDisplay < _pi.displays.length) { @@ -380,12 +388,22 @@ class ImageModel with ChangeNotifier { WeakReference parent; + final List _callbacksOnFirstImage = []; + ImageModel(this.parent); + addCallbackOnFirstImage(Function(String) cb) => + _callbacksOnFirstImage.add(cb); + onRgba(Uint8List rgba) { if (_waitForImage[id]!) { _waitForImage[id] = false; parent.target?.dialogManager.dismissAll(); + if (isDesktop) { + for (final cb in _callbacksOnFirstImage) { + cb(id); + } + } } final pid = parent.target?.id; ui.decodeImageFromPixels( @@ -495,7 +513,7 @@ class ViewStyle { double get scale { double s = 1.0; - if (style == 'adaptive') { + if (style == kRemoteViewStyleAdaptive) { final s1 = width / displayWidth; final s2 = height / displayHeight; s = s1 < s2 ? s1 : s2; @@ -511,6 +529,7 @@ class CanvasModel with ChangeNotifier { double _y = 0; // image scale double _scale = 1.0; + Size _size = Size.zero; // the tabbar over the image // double tabBarHeight = 0.0; // the window border's width @@ -524,6 +543,8 @@ class CanvasModel with ChangeNotifier { ScrollStyle _scrollStyle = ScrollStyle.scrollauto; ViewStyle _lastViewStyle = ViewStyle(); + final _imageOverflow = false.obs; + WeakReference parent; CanvasModel(this.parent); @@ -531,7 +552,12 @@ class CanvasModel with ChangeNotifier { double get x => _x; double get y => _y; double get scale => _scale; + Size get size => _size; ScrollStyle get scrollStyle => _scrollStyle; + ViewStyle get viewStyle => _lastViewStyle; + RxBool get imageOverflow => _imageOverflow; + + _resetScroll() => setScrollPercent(0.0, 0.0); setScrollPercent(double x, double y) { _scrollX = x; @@ -542,28 +568,44 @@ class CanvasModel with ChangeNotifier { double get scrollY => _scrollY; updateViewStyle() async { + Size getSize() { + final size = MediaQueryData.fromWindow(ui.window).size; + // If minimized, w or h may be negative here. + double w = size.width - windowBorderWidth * 2; + double h = size.height - tabBarHeight - windowBorderWidth * 2; + return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); + } + final style = await bind.sessionGetViewStyle(id: id); if (style == null) { return; } - final sizeWidth = size.width; - final sizeHeight = size.height; + + _size = getSize(); final displayWidth = getDisplayWidth(); final displayHeight = getDisplayHeight(); final viewStyle = ViewStyle( style: style, - width: sizeWidth, - height: sizeHeight, + width: size.width, + height: size.height, displayWidth: displayWidth, displayHeight: displayHeight, ); if (_lastViewStyle == viewStyle) { return; } + if (_lastViewStyle.style != viewStyle.style) { + _resetScroll(); + } _lastViewStyle = viewStyle; _scale = viewStyle.scale; - _x = (sizeWidth - displayWidth * _scale) / 2; - _y = (sizeHeight - displayHeight * _scale) / 2; + + if (kIgnoreDpi && style == kRemoteViewStyleOriginal) { + _scale = 1.0 / ui.window.devicePixelRatio; + } + _x = (size.width - displayWidth * _scale) / 2; + _y = (size.height - displayHeight * _scale) / 2; + _imageOverflow.value = _x < 0 || y < 0; notifyListeners(); } @@ -571,8 +613,7 @@ class CanvasModel with ChangeNotifier { final style = await bind.sessionGetScrollStyle(id: id); if (style == kRemoteScrollStyleBar) { _scrollStyle = ScrollStyle.scrollbar; - _scrollX = 0.0; - _scrollY = 0.0; + _resetScroll(); } else { _scrollStyle = ScrollStyle.scrollauto; } @@ -586,8 +627,8 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } - bool get cursorEmbeded => - parent.target?.ffiModel.display.cursorEmbeded ?? false; + bool get cursorEmbedded => + parent.target?.ffiModel.display.cursorEmbedded ?? false; int getDisplayWidth() { final defaultWidth = (isDesktop || isWebDesktop) @@ -606,14 +647,6 @@ class CanvasModel with ChangeNotifier { double get windowBorderWidth => stateGlobal.windowBorderWidth.value; double get tabBarHeight => stateGlobal.tabBarHeight; - Size get size { - final size = MediaQueryData.fromWindow(ui.window).size; - // If minimized, w or h may be negative here. - double w = size.width - windowBorderWidth * 2; - double h = size.height - tabBarHeight - windowBorderWidth * 2; - return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); - } - moveDesktopMouse(double x, double y) { // On mobile platforms, move the canvas with the cursor. final dw = getDisplayWidth() * _scale; @@ -1113,7 +1146,8 @@ class CursorModel with ChangeNotifier { _clearCache() { final keys = {...cachedKeys}; for (var k in keys) { - customCursorController.freeCache(k); + debugPrint("deleting cursor with key $k"); + CursorManager.instance.deleteCursor(k); } } } @@ -1220,6 +1254,7 @@ class FFI { late final ChatModel chatModel; // session late final FileModel fileModel; // session late final AbModel abModel; // global + late final GroupModel groupModel; // global late final UserModel userModel; // global late final QualityMonitorModel qualityMonitorModel; // session late final RecordingModel recordingModel; // recording @@ -1233,8 +1268,9 @@ class FFI { serverModel = ServerModel(WeakReference(this)); chatModel = ChatModel(WeakReference(this)); fileModel = FileModel(WeakReference(this)); - abModel = AbModel(WeakReference(this)); userModel = UserModel(WeakReference(this)); + abModel = AbModel(WeakReference(this)); + groupModel = GroupModel(WeakReference(this)); qualityMonitorModel = QualityMonitorModel(WeakReference(this)); recordingModel = RecordingModel(WeakReference(this)); inputModel = InputModel(WeakReference(this)); @@ -1318,7 +1354,7 @@ class Display { double y = 0; int width = 0; int height = 0; - bool cursorEmbeded = false; + bool cursorEmbedded = false; Display() { width = (isDesktop || isWebDesktop) diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index e6065743c..b0eebee53 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/common/widgets/peer_tab_page.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; @@ -10,17 +11,19 @@ import 'model.dart'; import 'platform_model.dart'; class UserModel { - var userName = ''.obs; + final RxString userName = ''.obs; + final RxString groupName = ''.obs; + final RxBool isAdmin = false.obs; WeakReference parent; - UserModel(this.parent) { - refreshCurrentUser(); - } + UserModel(this.parent); void refreshCurrentUser() async { - await getUserName(); final token = bind.mainGetLocalOption(key: 'access_token'); - if (token == '') return; + if (token == '') { + await _updateOtherModels(); + return; + } final url = await bind.mainGetApiServer(); final body = { 'id': await bind.mainGetMyId(), @@ -35,96 +38,95 @@ class UserModel { body: json.encode(body)); final status = response.statusCode; if (status == 401 || status == 400) { - resetToken(); + reset(); return; } - await _parseResp(response.body); + final data = json.decode(response.body); + final error = data['error']; + if (error != null) { + throw error; + } + + final user = UserPayload.fromJson(data); + await _parseAndUpdateUser(user); } catch (e) { print('Failed to refreshCurrentUser: $e'); + } finally { + await _updateOtherModels(); } } - void resetToken() async { + Future reset() async { await bind.mainSetLocalOption(key: 'access_token', value: ''); - await bind.mainSetLocalOption(key: 'user_info', value: ''); + await gFFI.abModel.reset(); + await gFFI.groupModel.reset(); userName.value = ''; + groupName.value = ''; + statePeerTab.check(); } - Future _parseResp(String body) async { - final data = json.decode(body); - final error = data['error']; - if (error != null) { - return error!; - } - final token = data['access_token']; - if (token != null) { - await bind.mainSetLocalOption(key: 'access_token', value: token); - } - final info = data['user']; - if (info != null) { - final value = json.encode(info); - await bind.mainSetOption(key: 'user_info', value: value); - userName.value = info['name']; - } - return ''; + Future _parseAndUpdateUser(UserPayload user) async { + userName.value = user.name; + groupName.value = user.grp; + isAdmin.value = user.isAdmin; } - Future getUserName() async { - if (userName.isNotEmpty) { - return userName.value; - } - final userInfo = bind.mainGetLocalOption(key: 'user_info'); - if (userInfo.trim().isEmpty) { - return ''; - } - final m = jsonDecode(userInfo); - if (m == null) { - userName.value = ''; - } else { - userName.value = m['name'] ?? ''; - } - return userName.value; + Future _updateOtherModels() async { + await gFFI.abModel.pullAb(); + await gFFI.groupModel.pull(); } Future logOut() async { final tag = gFFI.dialogManager.showLoading(translate('Waiting')); - final url = await bind.mainGetApiServer(); - final _ = await http.post(Uri.parse('$url/api/logout'), - body: { - 'id': await bind.mainGetMyId(), - 'uuid': await bind.mainGetUuid(), - }, - headers: await getHttpHeaders()); - await Future.wait([ - bind.mainSetLocalOption(key: 'access_token', value: ''), - bind.mainSetLocalOption(key: 'user_info', value: ''), - bind.mainSetLocalOption(key: 'selected-tags', value: ''), - ]); - parent.target?.abModel.clear(); - userName.value = ''; - gFFI.dialogManager.dismissByTag(tag); - } - - Future> login(String userName, String pass) async { - final url = await bind.mainGetApiServer(); try { - final resp = await http.post(Uri.parse('$url/api/login'), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'username': userName, - 'password': pass, - 'id': await bind.mainGetMyId(), - 'uuid': await bind.mainGetUuid() - })); - final body = jsonDecode(resp.body); - bind.mainSetLocalOption( - key: 'access_token', value: body['access_token'] ?? ''); - bind.mainSetLocalOption( - key: 'user_info', value: jsonEncode(body['user'])); - this.userName.value = body['user']?['name'] ?? ''; - return body; - } catch (err) { - return {'error': '$err'}; + final url = await bind.mainGetApiServer(); + await http + .post(Uri.parse('$url/api/logout'), + body: { + 'id': await bind.mainGetMyId(), + 'uuid': await bind.mainGetUuid(), + }, + headers: getHttpHeaders()) + .timeout(Duration(seconds: 2)); + } catch (e) { + print("request /api/logout failed: err=$e"); + } finally { + await reset(); + gFFI.dialogManager.dismissByTag(tag); } } + + /// throw [RequestException] + Future login(LoginRequest loginRequest) async { + final url = await bind.mainGetApiServer(); + final resp = await http.post(Uri.parse('$url/api/login'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(loginRequest.toJson())); + + final Map body; + try { + body = jsonDecode(resp.body); + } catch (e) { + print("jsonDecode resp body failed: ${e.toString()}"); + rethrow; + } + + if (resp.statusCode != 200) { + throw RequestException(resp.statusCode, body['error'] ?? ''); + } + + final LoginResponse loginResponse; + try { + loginResponse = LoginResponse.fromJson(body); + } catch (e) { + print("jsonDecode LoginResponse failed: ${e.toString()}"); + rethrow; + } + + if (loginResponse.user != null) { + await _parseAndUpdateUser(loginResponse.user!); + } + + return loginResponse; + } } diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 91cb9a08a..cf6d78cd2 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -1,6 +1,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/services.dart'; import 'package:flutter_hbb/common.dart'; @@ -34,7 +35,7 @@ class RustDeskMultiWindowManager { static final instance = RustDeskMultiWindowManager._(); final List _activeWindows = List.empty(growable: true); - final List _windowActiveCallbacks = List.empty(growable: true); + final List _windowActiveCallbacks = List.empty(growable: true); int? _remoteDesktopWindowId; int? _fileTransferWindowId; int? _portForwardWindowId; @@ -191,41 +192,41 @@ class RustDeskMultiWindowManager { return _activeWindows; } - void _notifyActiveWindow() { + Future _notifyActiveWindow() async { for (final callback in _windowActiveCallbacks) { - callback.call(); + await callback.call(); } } - void registerActiveWindow(int windowId) { + Future registerActiveWindow(int windowId) async { if (_activeWindows.contains(windowId)) { // ignore } else { _activeWindows.add(windowId); } - _notifyActiveWindow(); + await _notifyActiveWindow(); } /// Remove active window which has [`windowId`] /// - /// [Avaliability] + /// [Availability] /// This function should only be called from main window. /// For other windows, please post a unregister(hide) event to main window handler: /// `rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId!});` - void unregisterActiveWindow(int windowId) { + Future unregisterActiveWindow(int windowId) async { if (!_activeWindows.contains(windowId)) { // ignore } else { _activeWindows.remove(windowId); } - _notifyActiveWindow(); + await _notifyActiveWindow(); } - void registerActiveWindowListener(VoidCallback callback) { + void registerActiveWindowListener(AsyncCallback callback) { _windowActiveCallbacks.add(callback); } - void unregisterActiveWindowListener(VoidCallback callback) { + void unregisterActiveWindowListener(AsyncCallback callback) { _windowActiveCallbacks.remove(callback); } } diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 215c6f0ee..21e25fa28 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -23,7 +23,15 @@ static void my_application_activate(GApplication* application) { GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // we have custom window frame gtk_window_set_decorated(window, FALSE); - + // try setting icon for rustdesk, which uses the system cache + GtkIconTheme* theme = gtk_icon_theme_get_default(); + gint icons[4] = {256, 128, 64, 32}; + for (int i = 0; i < 4; i++) { + GdkPixbuf* icon = gtk_icon_theme_load_icon(theme, "rustdesk", icons[i], GTK_ICON_LOOKUP_NO_SVG, NULL); + if (icon != nullptr) { + gtk_window_set_icon(window, icon); + } + } // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu // desktop). diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index ec4baf141..a8b5306be 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */; }; + 7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */; }; 84010BA8292CF66600152837 /* liblibrustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (Weak, ); }; }; 84010BA9292CF68300152837 /* liblibrustdesk.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; }; @@ -74,6 +76,8 @@ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light.png"; path = "../../res/mac-tray-light.png"; sourceTree = ""; }; + 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark.png"; path = "../../res/mac-tray-dark.png"; sourceTree = ""; }; 84010BA7292CF66600152837 /* liblibrustdesk.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = liblibrustdesk.dylib; path = ../../target/release/liblibrustdesk.dylib; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; @@ -127,6 +131,8 @@ 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( + 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */, + 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */, 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, @@ -253,6 +259,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */, + 7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */, 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, ); @@ -378,7 +386,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = x86_64; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -403,6 +411,7 @@ CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -428,8 +437,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -459,7 +471,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = x86_64; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -513,7 +525,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = x86_64; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -538,6 +550,7 @@ CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -550,6 +563,12 @@ MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ( + "-sectcreate", + __CGPreLoginApp, + __cgpreloginapp, + /dev/null, + ); SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -563,8 +582,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -590,8 +611,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -602,6 +626,12 @@ ../../target/release, ); MACOSX_DEPLOYMENT_TARGET = 10.14; + OTHER_LDFLAGS = ( + "-sectcreate", + __CGPreLoginApp, + __cgpreloginapp, + /dev/null, + ); PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 9a3310e01..f51492c53 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 1e8713fda..97555e4a2 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index a529bd927..9ea846547 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index 7d06c1d80..c695fd3a3 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 2ce172957..a99d7db5a 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index 5c192efb8..5ad5a6bcd 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index c9bf7b6bb..645580310 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/flutter/macos/Runner/DebugProfile.entitlements b/flutter/macos/Runner/DebugProfile.entitlements index 9f56413f3..b52c39df4 100644 --- a/flutter/macos/Runner/DebugProfile.entitlements +++ b/flutter/macos/Runner/DebugProfile.entitlements @@ -6,6 +6,8 @@ com.apple.security.cs.allow-jit + com.apple.security.device.audio-input + com.apple.security.network.server diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 1d16763ee..540cd9ab9 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -49,7 +49,8 @@ class MainFlutterWindow: NSWindow { super.awakeFromNib() } -// override func bitsdojo_window_configure() -> UInt { -// return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP -// } + override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { + super.order(place, relativeTo: otherWin) + hiddenWindowAtLaunch() + } } diff --git a/flutter/macos/Runner/Release.entitlements b/flutter/macos/Runner/Release.entitlements index 08ba3a3fa..7f588d928 100644 --- a/flutter/macos/Runner/Release.entitlements +++ b/flutter/macos/Runner/Release.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.cs.allow-jit + + com.apple.security.device.audio-input + com.apple.security.network.client diff --git a/flutter/macos/rustdesk.xcodeproj/project.pbxproj b/flutter/macos/rustdesk.xcodeproj/project.pbxproj index e334f0ac5..664f88618 100644 --- a/flutter/macos/rustdesk.xcodeproj/project.pbxproj +++ b/flutter/macos/rustdesk.xcodeproj/project.pbxproj @@ -108,6 +108,12 @@ PRODUCT_NAME = rustdesk; SDKROOT = macosx; SUPPORTS_MACCATALYST = YES; + OTHER_LDFLAGS = ( + "-sectcreate", + __CGPreLoginApp, + __cgpreloginapp, + /dev/null, + ); }; name = Release; }; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 625862dbf..807f932bb 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -105,7 +105,7 @@ packages: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.3.2" + version: "2.3.3" build_runner_core: dependency: transitive description: @@ -182,7 +182,7 @@ packages: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "4.3.0" + version: "4.4.0" collection: dependency: transitive description: @@ -352,7 +352,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "5.2.2" + version: "5.2.4" fixnum: dependency: transitive description: @@ -389,12 +389,10 @@ packages: flutter_custom_cursor: dependency: "direct main" description: - path: "." - ref: "74b1b314142b6775c1243067a3503ac568ebc74b" - resolved-ref: "74b1b314142b6775c1243067a3503ac568ebc74b" - url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" - source: git - version: "0.0.1" + name: flutter_custom_cursor + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2" flutter_improved_scrolling: dependency: "direct main" description: @@ -455,7 +453,7 @@ packages: name: freezed url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.3.2" freezed_annotation: dependency: "direct main" description: @@ -546,7 +544,7 @@ packages: name: image_picker_android url: "https://pub.dartlang.org" source: hosted - version: "0.8.5+3" + version: "0.8.5+4" image_picker_for_web: dependency: transitive description: @@ -560,7 +558,7 @@ packages: name: image_picker_ios url: "https://pub.dartlang.org" source: hosted - version: "0.8.6+1" + version: "0.8.6+3" image_picker_platform_interface: dependency: transitive description: @@ -637,7 +635,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" nested: dependency: transitive description: @@ -826,7 +824,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.4" + version: "6.0.5" pub_semver: dependency: transitive description: @@ -1113,35 +1111,35 @@ packages: name: video_player url: "https://pub.dartlang.org" source: hosted - version: "2.4.8" + version: "2.4.10" video_player_android: dependency: transitive description: name: video_player_android url: "https://pub.dartlang.org" source: hosted - version: "2.3.9" + version: "2.3.10" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation url: "https://pub.dartlang.org" source: hosted - version: "2.3.7" + version: "2.3.8" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "5.1.4" + version: "6.0.1" video_player_web: dependency: transitive description: name: video_player_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.12" + version: "2.0.13" visibility_detector: dependency: "direct main" description: @@ -1204,7 +1202,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "3.1.2" + version: "3.1.3" win32_registry: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a8a3d7050..705f4650c 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -63,12 +63,9 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 82f9eab81cb2c7bfb938def7a1b399a6279bbc75 + ref: 057e6eb1bc7dcbcf9dafd1384274a611e4fe7124 freezed_annotation: ^2.0.3 - flutter_custom_cursor: - git: - url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor - ref: 74b1b314142b6775c1243067a3503ac568ebc74b + flutter_custom_cursor: ^0.0.2 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git diff --git a/flutter/web/icons/Icon-192.png b/flutter/web/icons/Icon-192.png index db3e76713..5d4566850 100644 Binary files a/flutter/web/icons/Icon-192.png and b/flutter/web/icons/Icon-192.png differ diff --git a/flutter/web/icons/Icon-512.png b/flutter/web/icons/Icon-512.png index 6910947a3..2b1abc3f2 100644 Binary files a/flutter/web/icons/Icon-512.png and b/flutter/web/icons/Icon-512.png differ diff --git a/flutter/web/icons/Icon-maskable-192.png b/flutter/web/icons/Icon-maskable-192.png index 36597c1bc..30147e96e 100644 Binary files a/flutter/web/icons/Icon-maskable-192.png and b/flutter/web/icons/Icon-maskable-192.png differ diff --git a/flutter/web/icons/Icon-maskable-512.png b/flutter/web/icons/Icon-maskable-512.png index f2f79e64b..e84ca5bc7 100644 Binary files a/flutter/web/icons/Icon-maskable-512.png and b/flutter/web/icons/Icon-maskable-512.png differ diff --git a/flutter/web/js/gen_js_from_hbb.py b/flutter/web/js/gen_js_from_hbb.py index 0bdde54e4..8ee553b35 100755 --- a/flutter/web/js/gen_js_from_hbb.py +++ b/flutter/web/js/gen_js_from_hbb.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import re +import re import os import glob from tabnanny import check @@ -69,7 +69,7 @@ def main(): 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) diff --git a/flutter/web/js/src/codec.js b/flutter/web/js/src/codec.js index dc579b5f3..27c9565ec 100644 --- a/flutter/web/js/src/codec.js +++ b/flutter/web/js/src/codec.js @@ -22,8 +22,8 @@ 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, + // 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(); diff --git a/flutter/windows/runner/win32_window.cpp b/flutter/windows/runner/win32_window.cpp index 9ada9ab2e..2ff6d686c 100644 --- a/flutter/windows/runner/win32_window.cpp +++ b/flutter/windows/runner/win32_window.cpp @@ -116,7 +116,7 @@ bool Win32Window::CreateAndShow(const std::wstring& title, HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; - + HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), diff --git a/libs/clipboard/docs/assets/scene3.png b/libs/clipboard/docs/assets/scene3.png index 639236460..a65a7ac06 100644 Binary files a/libs/clipboard/docs/assets/scene3.png and b/libs/clipboard/docs/assets/scene3.png differ diff --git a/libs/clipboard/docs/assets/win_A_B.png b/libs/clipboard/docs/assets/win_A_B.png index 0b6b25d0e..b65e571e7 100644 Binary files a/libs/clipboard/docs/assets/win_A_B.png and b/libs/clipboard/docs/assets/win_A_B.png differ diff --git a/libs/clipboard/docs/assets/win_B_A.png b/libs/clipboard/docs/assets/win_B_A.png index 377fa801f..4484b21ac 100644 Binary files a/libs/clipboard/docs/assets/win_B_A.png and b/libs/clipboard/docs/assets/win_B_A.png differ diff --git a/libs/enigo/.vscode/launch.json b/libs/enigo/.vscode/launch.json index a7a40dcfe..123e0bc42 100644 --- a/libs/enigo/.vscode/launch.json +++ b/libs/enigo/.vscode/launch.json @@ -1,7 +1,7 @@ { "version": "0.2.0", "configurations": [ - + { "name": "Debug", "type": "gdb", diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index 83c79e064..cc4173a97 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -22,8 +22,8 @@ appveyor = { repository = "pythoneer/enigo-85xiy" } serde = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true } log = "0.4" -rdev = { git = "https://github.com/asur4s/rdev" } -tfc = { git = "https://github.com/asur4s/The-Fat-Controller" } +rdev = { git = "https://github.com/fufesou/rdev" } +tfc = { git = "https://github.com/fufesou/The-Fat-Controller" } hbb_common = { path = "../hbb_common" } [features] diff --git a/libs/enigo/examples/mouse.rs b/libs/enigo/examples/mouse.rs index 50a3506cf..f963e041e 100644 --- a/libs/enigo/examples/mouse.rs +++ b/libs/enigo/examples/mouse.rs @@ -23,15 +23,18 @@ fn main() { enigo.mouse_click(MouseButton::Left); thread::sleep(wait_time); - enigo.mouse_scroll_x(2); - thread::sleep(wait_time); + #[cfg(not(target_os = "macos"))] + { + enigo.mouse_scroll_x(2); + thread::sleep(wait_time); - enigo.mouse_scroll_x(-2); - thread::sleep(wait_time); + enigo.mouse_scroll_x(-2); + thread::sleep(wait_time); - enigo.mouse_scroll_y(2); - thread::sleep(wait_time); + enigo.mouse_scroll_y(2); + thread::sleep(wait_time); - enigo.mouse_scroll_y(-2); - thread::sleep(wait_time); + enigo.mouse_scroll_y(-2); + thread::sleep(wait_time); + } } diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index 083345e63..fcc2981fd 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -21,7 +21,7 @@ //! Possible use cases could be for testing user interfaces on different //! plattforms, //! building remote control applications or just automating tasks for user -//! interfaces unaccessible by a public API or scripting laguage. +//! interfaces unaccessible by a public API or scripting language. //! //! For the keyboard there are currently two modes you can use. The first mode //! is represented by the [key_sequence]() function @@ -104,6 +104,10 @@ pub enum MouseButton { Middle, /// Right mouse button Right, + /// Back mouse button + Back, + /// Forward mouse button + Forward, /// Scroll up button ScrollUp, @@ -447,8 +451,9 @@ pub trait KeyboardControllable { where Self: Sized, { - self.key_sequence_parse_try(sequence) - .expect("Could not parse sequence"); + if let Err(..) = self.key_sequence_parse_try(sequence) { + println!("Could not parse sequence"); + } } /// Same as key_sequence_parse except returns any errors fn key_sequence_parse_try(&mut self, sequence: &str) -> Result<(), dsl::ParseError> diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index 4eb890c29..47e6d53c0 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -88,7 +88,13 @@ impl Default for Enigo { Self { is_x11, tfc: if is_x11 { - Some(TFC_Context::new().expect("kbd context error")) + match TFC_Context::new() { + Ok(ctx) => Some(ctx), + Err(..) => { + println!("kbd context error"); + None + } + } } else { None }, diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs index ed2d28dc1..2115d7283 100644 --- a/libs/enigo/src/linux/xdo.rs +++ b/libs/enigo/src/linux/xdo.rs @@ -57,6 +57,8 @@ fn mousebutton(button: MouseButton) -> c_int { MouseButton::ScrollDown => 5, MouseButton::ScrollLeft => 6, MouseButton::ScrollRight => 7, + MouseButton::Back => 8, + MouseButton::Forward => 9, } } @@ -391,8 +393,9 @@ impl KeyboardControllable for EnigoXdo { where Self: Sized, { - self.key_sequence_parse_try(sequence) - .expect("Could not parse sequence"); + if let Err(..) = self.key_sequence_parse_try(sequence) { + println!("Could not parse sequence"); + } } fn key_sequence_parse_try(&mut self, sequence: &str) -> Result<(), crate::dsl::ParseError> diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index 937320f7d..55f350895 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -40,6 +40,7 @@ const BUF_LEN: usize = 4; #[allow(improper_ctypes)] #[allow(non_snake_case)] #[link(name = "ApplicationServices", kind = "framework")] +#[link(name = "Carbon", kind = "framework")] extern "C" { fn CFDataGetBytePtr(theData: CFDataRef) -> *const u8; fn TISCopyCurrentKeyboardInputSource() -> TISInputSourceRef; @@ -68,7 +69,7 @@ extern "C" { ) -> Boolean; fn CGEventPost(tapLocation: CGEventTapLocation, event: *mut MyCGEvent); - // Actually return CFDataRef which is const here, but for coding convienence, return *mut c_void + // Actually return CFDataRef which is const here, but for coding convenience, return *mut c_void fn TISGetInputSourceProperty(source: TISInputSourceRef, property: *const c_void) -> *mut c_void; // not present in servo/core-graphics @@ -225,7 +226,10 @@ impl MouseControllable for Enigo { MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown), MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown), MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown), - _ => unimplemented!(), + _ => { + log::info!("Unsupported button {:?}", button); + return Ok(()); + }, }; let dest = CGPoint::new(current_x as f64, current_y as f64); if let Some(src) = self.event_source.as_ref() { @@ -248,7 +252,10 @@ impl MouseControllable for Enigo { MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp), MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp), MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp), - _ => unimplemented!(), + _ => { + log::info!("Unsupported button {:?}", button); + return; + }, }; let dest = CGPoint::new(current_x as f64, current_y as f64); if let Some(src) = self.event_source.as_ref() { diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index 4a4fd7fc4..2e1108b9e 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -56,6 +56,20 @@ fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { input.type_ = INPUT_KEYBOARD; unsafe { let dst_ptr = (&mut input.u as *mut _) as *mut u8; + let flags = match vk as _ { + winapi::um::winuser::VK_HOME | + winapi::um::winuser::VK_UP | + winapi::um::winuser::VK_PRIOR | + winapi::um::winuser::VK_LEFT | + winapi::um::winuser::VK_RIGHT | + winapi::um::winuser::VK_END | + winapi::um::winuser::VK_DOWN | + winapi::um::winuser::VK_NEXT | + winapi::um::winuser::VK_INSERT | + winapi::um::winuser::VK_DELETE => flags | winapi::um::winuser::KEYEVENTF_EXTENDEDKEY, + _ => flags, + }; + let k = KEYBDINPUT { wVk: vk, wScan: scan, @@ -134,9 +148,18 @@ impl MouseControllable for Enigo { MouseButton::Left => MOUSEEVENTF_LEFTDOWN, MouseButton::Middle => MOUSEEVENTF_MIDDLEDOWN, MouseButton::Right => MOUSEEVENTF_RIGHTDOWN, - _ => unimplemented!(), + MouseButton::Back => MOUSEEVENTF_XDOWN, + MouseButton::Forward => MOUSEEVENTF_XDOWN, + _ => { + log::info!("Unsupported button {:?}", button); + return Ok(()); + } + }, + match button { + MouseButton::Back => XBUTTON1 as _, + MouseButton::Forward => XBUTTON2 as _, + _ => 0, }, - 0, 0, 0, ); @@ -155,9 +178,18 @@ impl MouseControllable for Enigo { MouseButton::Left => MOUSEEVENTF_LEFTUP, MouseButton::Middle => MOUSEEVENTF_MIDDLEUP, MouseButton::Right => MOUSEEVENTF_RIGHTUP, - _ => unimplemented!(), + MouseButton::Back => MOUSEEVENTF_XUP, + MouseButton::Forward => MOUSEEVENTF_XUP, + _ => { + log::info!("Unsupported button {:?}", button); + return; + } + }, + match button { + MouseButton::Back => XBUTTON1 as _, + MouseButton::Forward => XBUTTON2 as _, + _ => 0, }, - 0, 0, 0, ); diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 9217388aa..650e42104 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -40,7 +40,7 @@ message DisplayInfo { int32 height = 4; string name = 5; bool online = 6; - bool cursor_embeded = 7; + bool cursor_embedded = 7; } message PortForward { @@ -420,7 +420,7 @@ message SwitchDisplay { sint32 y = 3; int32 width = 4; int32 height = 5; - bool cursor_embeded = 6; + bool cursor_embedded = 6; } message PermissionInfo { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 328a1ea59..1d427a2e9 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, fs, - net::{IpAddr, Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, path::{Path, PathBuf}, sync::{Arc, Mutex, RwLock}, time::SystemTime, @@ -203,6 +203,8 @@ pub struct PeerConfig { pub enable_file_transfer: bool, #[serde(default)] pub show_quality_monitor: bool, + #[serde(default)] + pub keyboard_mode: String, // The other scalar value must before this #[serde(default, deserialize_with = "PeerConfig::deserialize_options")] @@ -509,8 +511,12 @@ impl Config { } #[inline] - pub fn get_any_listen_addr() -> SocketAddr { - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0) + 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 { @@ -991,6 +997,8 @@ pub struct LocalConfig { #[serde(default)] remote_id: String, // latest used one #[serde(default)] + kb_layout_type: String, + #[serde(default)] size: Size, #[serde(default)] pub fav: Vec, @@ -1010,6 +1018,16 @@ impl LocalConfig { 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 } diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index e08414324..fec8b8670 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -578,7 +578,7 @@ impl TransferJob { /// /// [`Note`] /// Conditions: - /// 1. Files are not waiting for comfirmation by peers. + /// 1. Files are not waiting for confirmation by peers. #[inline] pub fn job_completed(&self) -> bool { // has no error, Condition 2 diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index ae564685f..85e0100d9 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -10,7 +10,7 @@ pub use protos::rendezvous as rendezvous_proto; use std::{ fs::File, io::{self, BufRead}, - net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, path::Path, time::{self, SystemTime, UNIX_EPOCH}, }; @@ -66,6 +66,21 @@ macro_rules! allow_err { } else { } }; + + ($e:expr, $($arg:tt)*) => { + if let Err(err) = $e { + log::debug!( + "{:?}, {}, {}:{}:{}:{}", + err, + format_args!($($arg)*), + module_path!(), + file!(), + line!(), + column!() + ); + } else { + } + }; } #[inline] @@ -102,13 +117,31 @@ impl AddrMangle { } bytes[..(16 - n_padding)].to_vec() } - _ => { - panic!("Only support ipv4"); + 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 { + if bytes.len() > 16 { + if bytes.len() != 18 { + return Config::get_any_listen_addr(false); + } + #[allow(invalid_value)] + let mut tmp: [u8; 2] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + tmp.copy_from_slice(&bytes[16..]); + let port = u16::from_le_bytes(tmp); + #[allow(invalid_value)] + let mut tmp: [u8; 16] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + tmp.copy_from_slice(&bytes[..16]); + 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); @@ -249,5 +282,61 @@ mod tests { 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" + ); + } +} + +#[inline] +pub fn is_ipv4_str(id: &str) -> bool { + regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$") + .unwrap() + .is_match(id) +} + +#[inline] +pub fn is_ipv6_str(id: &str) -> bool { + 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+))$") + .unwrap() + .is_match(id) +} + +#[inline] +pub fn is_ip_str(id: &str) -> bool { + is_ipv4_str(id) || is_ipv6_str(id) +} + +#[cfg(test)] +mod test_lib { + use super::*; + + #[test] + fn test_ipv6() { + assert_eq!(is_ipv6_str("1:2:3"), true); + assert_eq!(is_ipv6_str("[ab:2:3]:12"), true); + assert_eq!(is_ipv6_str("[ABEF:2a:3]:12"), true); + assert_eq!(is_ipv6_str("[ABEG:2a:3]:12"), false); + assert_eq!(is_ipv6_str("1[ab:2:3]:12"), false); + assert_eq!(is_ipv6_str("1.1.1.1"), false); + assert_eq!(is_ip_str("1.1.1.1"), true); + assert_eq!(is_ipv6_str("1:2:"), false); + assert_eq!(is_ipv6_str("1:2::0"), true); + assert_eq!(is_ipv6_str("[1:2::0]:1"), true); + assert_eq!(is_ipv6_str("[1:2::0]:"), false); + assert_eq!(is_ipv6_str("1:2::0]:1"), false); } } diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index 72ab73f16..6f62163d1 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -9,31 +9,52 @@ use std::net::SocketAddr; use tokio::net::ToSocketAddrs; use tokio_socks::{IntoTargetAddr, TargetAddr}; -fn to_socket_addr(host: &str) -> ResultType { - use std::net::ToSocketAddrs; - host.to_socket_addrs()? - .filter(|x| x.is_ipv4()) - .next() - .context("Failed to solve") +#[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); + } + return host; } -pub fn get_target_addr(host: &str) -> ResultType> { - let addr = match Config::get_network_type() { - NetworkType::Direct => to_socket_addr(&host)?.into_target_addr()?, - NetworkType::ProxySocks => host.into_target_addr()?, +#[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); + } + } } - .to_owned(); - Ok(addr) + return host; } pub fn test_if_valid_server(host: &str) -> String { - let mut host = host.to_owned(); - if !host.contains(":") { - host = format!("{}:{}", host, 0); - } + let host = check_port(host, 0); + use std::net::ToSocketAddrs; match Config::get_network_type() { - NetworkType::Direct => match to_socket_addr(&host) { + NetworkType::Direct => match host.to_socket_addrs() { Err(err) => err.to_string(), Ok(_) => "".to_owned(), }, @@ -44,33 +65,126 @@ pub fn test_if_valid_server(host: &str) -> String { } } -pub async fn connect_tcp<'t, T: IntoTargetAddr<'t>>( +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, - local: SocketAddr, ms_timeout: u64, ) -> ResultType { - let target_addr = target.into_target_addr()?; + 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() { - FramedStream::connect( + return FramedStream::connect( conf.proxy.as_str(), - target_addr, + target, local, conf.username.as_str(), conf.password.as_str(), ms_timeout, ) - .await - } else { - let addr = std::net::ToSocketAddrs::to_socket_addrs(&target_addr)? - .filter(|x| x.is_ipv4()) - .next() - .context("Invalid target addr, no valid ipv4 address can be resolved.")?; - Ok(FramedStream::new(addr, local, ms_timeout).await?) + .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 Ok(FramedStream::new(target, Some(local), ms_timeout).await?); + } + } + } + Ok(FramedStream::new(target, local, ms_timeout).await?) +} + +#[inline] +pub fn is_ipv4(target: &TargetAddr<'_>) -> bool { + match target { + TargetAddr::Ip(addr) => addr.is_ipv4(), + _ => true, } } -pub async fn new_udp(local: T, ms_timeout: u64) -> ResultType { +#[inline] +pub async fn query_nip_io(addr: &SocketAddr) -> ResultType { + tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port())) + .await? + .filter(|x| x.is_ipv6()) + .next() + .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!("{}.nip.io", ip)); + } + } + 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) => { @@ -87,9 +201,82 @@ pub async fn new_udp(local: T, ms_timeout: u64) -> ResultType< } } -pub async fn rebind_udp(local: T) -> ResultType> { - match Config::get_network_type() { - NetworkType::Direct => Ok(Some(FramedSocket::new(local).await?)), - _ => Ok(None), +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").is_empty()); + // on Linux, "1" is resolved to "0.0.0.1" + assert!(test_if_valid_server("1.1.1.1").is_empty()); + assert!(test_if_valid_server("1.1.1.1: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 index 7966920c2..a1322fc15 100644 --- a/libs/hbb_common/src/tcp.rs +++ b/libs/hbb_common/src/tcp.rs @@ -5,7 +5,7 @@ use protobuf::Message; use sodiumoxide::crypto::secretbox::{self, Key, Nonce}; use std::{ io::{self, Error, ErrorKind}, - net::SocketAddr, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, ops::{Deref, DerefMut}, pin::Pin, task::{Context, Poll}, @@ -73,73 +73,79 @@ fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result( - remote_addr: T1, - local_addr: T2, + pub async fn new( + remote_addr: T, + local_addr: Option, ms_timeout: u64, ) -> ResultType { - for local_addr in lookup_host(&local_addr).await? { - for remote_addr in lookup_host(&remote_addr).await? { - let stream = super::timeout( - ms_timeout, - new_socket(local_addr, true)?.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, - )); + 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!("could not resolve to any address"); + bail!(format!("Failed to connect to {}", remote_addr)); } - pub async fn connect<'a, 't, P, T1, T2>( + pub async fn connect<'a, 't, P, T>( proxy: P, - target: T1, - local: T2, + target: T, + local_addr: Option, username: &'a str, password: &'a str, ms_timeout: u64, ) -> ResultType where P: ToProxyAddrs, - T1: IntoTargetAddr<'t>, - T2: ToSocketAddrs, + T: IntoTargetAddr<'t>, { - if let Some(local) = lookup_host(&local).await?.next() { - if let Some(proxy) = proxy.to_proxy_addrs().next().await { - let stream = - super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy?)).await??; - stream.set_nodelay(true).ok(); - let stream = if username.trim().is_empty() { - super::timeout( - ms_timeout, - Socks5Stream::connect_with_socket(stream, target), - ) - .await?? - } else { - super::timeout( - ms_timeout, - Socks5Stream::connect_with_password_and_socket( - stream, target, username, password, - ), - ) - .await?? - }; - let addr = stream.local_addr()?; - return Ok(Self( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )); + if let Some(Ok(proxy)) = proxy.to_proxy_addrs().next().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(ms_timeout, new_socket(local, true)?.connect(proxy)).await??; + stream.set_nodelay(true).ok(); + let stream = if username.trim().is_empty() { + super::timeout( + ms_timeout, + Socks5Stream::connect_with_socket(stream, target), + ) + .await?? + } else { + super::timeout( + ms_timeout, + Socks5Stream::connect_with_password_and_socket( + stream, target, username, password, + ), + ) + .await?? + }; + let addr = stream.local_addr()?; + return Ok(Self( + Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + addr, + None, + 0, + )); + } bail!("could not resolve to any address"); } @@ -252,6 +258,38 @@ pub async fn new_listener(addr: T, reuse: bool) -> ResultType< } } +pub async fn listen_any(port: u16) -> ResultType { + if let Ok(mut socket) = TcpSocket::new_v6() { + #[cfg(unix)] + { + 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); + } + } + } + let s = TcpSocket::new_v4()?; + s.bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port))?; + Ok(s.listen(DEFAULT_BACKLOG)?) +} + impl Unpin for DynTcpStream {} impl AsyncRead for DynTcpStream { diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs index 3532dd1e0..38121a4e1 100644 --- a/libs/hbb_common/src/udp.rs +++ b/libs/hbb_common/src/udp.rs @@ -49,7 +49,7 @@ impl FramedSocket { #[allow(clippy::never_loop)] pub async fn new_reuse(addr: T) -> ResultType { - for addr in addr.to_socket_addrs()?.filter(|x| x.is_ipv4()) { + for addr in addr.to_socket_addrs()? { let socket = new_socket(addr, true, 0)?.into_udp_socket(); return Ok(Self::Direct(UdpFramed::new( UdpSocket::from_std(socket)?, @@ -63,7 +63,7 @@ impl FramedSocket { addr: T, buf_size: usize, ) -> ResultType { - for addr in addr.to_socket_addrs()?.filter(|x| x.is_ipv4()) { + for addr in addr.to_socket_addrs()? { return Ok(Self::Direct(UdpFramed::new( UdpSocket::from_std(new_socket(addr, false, buf_size)?.into_udp_socket())?, BytesCodec::new(), @@ -164,4 +164,13 @@ impl FramedSocket { None } } + + pub fn is_ipv4(&self) -> bool { + if let FramedSocket::Direct(x) = self { + if let Ok(v) = x.get_ref().local_addr() { + return v.is_ipv4(); + } + } + true + } } diff --git a/libs/portable/src/bin_reader.rs b/libs/portable/src/bin_reader.rs index 499c18e2c..2d0b1bf7e 100644 --- a/libs/portable/src/bin_reader.rs +++ b/libs/portable/src/bin_reader.rs @@ -4,7 +4,10 @@ use std::{ path::PathBuf, }; +#[cfg(windows)] const BIN_DATA: &[u8] = include_bytes!("../data.bin"); +#[cfg(not(windows))] +const BIN_DATA: &[u8] = &[]; // 4bytes const LENGTH: usize = 4; const IDENTIFIER_LENGTH: usize = 8; @@ -118,7 +121,7 @@ impl BinaryReader { (parsed, executable) } - #[cfg(unix)] + #[cfg(linux)] pub fn configure_permission(&self, prefix: &PathBuf) { use std::os::unix::prelude::PermissionsExt; diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs index edcbdd1fd..13dd0c3dc 100644 --- a/libs/portable/src/main.rs +++ b/libs/portable/src/main.rs @@ -30,7 +30,7 @@ fn setup(reader: BinaryReader, dir: Option, clear: bool) -> Option, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl Capturer { diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index d729342d6..9535e9f3a 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -218,7 +218,7 @@ impl Encoder { #[inline] pub fn current_hw_encoder_name() -> Option { #[cfg(feature = "hwcodec")] - if check_hwcodec_config() { + if enable_hwcodec_option() { return HwEncoder::current_name().lock().unwrap().clone(); } else { return None; @@ -229,7 +229,7 @@ impl Encoder { pub fn supported_encoding() -> (bool, bool) { #[cfg(feature = "hwcodec")] - if check_hwcodec_config() { + if enable_hwcodec_option() { let best = HwEncoder::best(); ( best.h264.as_ref().map_or(false, |c| c.score > 0), @@ -246,7 +246,7 @@ impl Encoder { impl Decoder { pub fn video_codec_state(_id: &str) -> VideoCodecState { #[cfg(feature = "hwcodec")] - if check_hwcodec_config() { + if enable_hwcodec_option() { let best = HwDecoder::best(); return VideoCodecState { score_vpx: SCORE_VPX, @@ -257,7 +257,7 @@ impl Decoder { }; } #[cfg(feature = "mediacodec")] - if check_hwcodec_config() { + if enable_hwcodec_option() { let score_h264 = if H264_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) { 92 } else { @@ -287,11 +287,19 @@ impl Decoder { Decoder { vpx, #[cfg(feature = "hwcodec")] - hw: HwDecoder::new_decoders(), + hw: if enable_hwcodec_option() { + HwDecoder::new_decoders() + } else { + HwDecoders::default() + }, #[cfg(feature = "hwcodec")] i420: vec![], #[cfg(feature = "mediacodec")] - media_codec: MediaCodecDecoder::new_decoders(), + media_codec: if enable_hwcodec_option() { + MediaCodecDecoder::new_decoders() + } else { + MediaCodecDecoders::default() + }, } } @@ -415,7 +423,7 @@ impl Decoder { } #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] -fn check_hwcodec_config() -> bool { +fn enable_hwcodec_option() -> bool { if let Some(v) = Config2::get().options.get("enable-hwcodec") { return v != "N"; } diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 166f7516c..c77da3f8f 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -94,7 +94,7 @@ impl EncoderApi for HwEncoder { frames.push(EncodedVideoFrame { data: Bytes::from(frame.data), pts: frame.pts as _, - key:frame.key == 1, + key: frame.key == 1, ..Default::default() }); } @@ -175,6 +175,7 @@ pub struct HwDecoder { pub info: CodecInfo, } +#[derive(Default)] pub struct HwDecoders { pub h264: Option, pub h265: Option, diff --git a/libs/scrap/src/common/mediacodec.rs b/libs/scrap/src/common/mediacodec.rs index fa821246c..406baecb5 100644 --- a/libs/scrap/src/common/mediacodec.rs +++ b/libs/scrap/src/common/mediacodec.rs @@ -37,6 +37,7 @@ impl Deref for MediaCodecDecoder { } } +#[derive(Default)] pub struct MediaCodecDecoders { pub h264: Option, pub h265: Option, diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 82f65537b..1de2f89d6 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -44,8 +44,8 @@ pub mod record; mod vpx; #[inline] -pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> { - let b = unsafe { std::slice::from_raw_parts::(b.as_ptr() as _, b.len() / 16) }; +pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> { + // does this really help? if b == &old[..] { return Err(std::io::ErrorKind::WouldBlock.into()); } @@ -72,16 +72,16 @@ pub fn is_x11() -> bool { #[cfg(x11)] #[inline] -pub fn is_cursor_embeded() -> bool { +pub fn is_cursor_embedded() -> bool { if is_x11() { - x11::IS_CURSOR_EMBEDED + x11::IS_CURSOR_EMBEDDED } else { - wayland::IS_CURSOR_EMBEDED + wayland::IS_CURSOR_EMBEDDED } } #[cfg(not(x11))] #[inline] -pub fn is_cursor_embeded() -> bool { +pub fn is_cursor_embedded() -> bool { false } diff --git a/libs/scrap/src/common/quartz.rs b/libs/scrap/src/common/quartz.rs index 6e29c2441..a02d55ebb 100644 --- a/libs/scrap/src/common/quartz.rs +++ b/libs/scrap/src/common/quartz.rs @@ -8,7 +8,7 @@ pub struct Capturer { frame: Arc>>, use_yuv: bool, i420: Vec, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl Capturer { diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index 2593e56fe..e625fca7e 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -4,7 +4,7 @@ use std::{io, sync::RwLock, time::Duration}; pub struct Capturer(Display, Box, bool, Vec); -pub const IS_CURSOR_EMBEDED: bool = true; +pub const IS_CURSOR_EMBEDDED: bool = true; lazy_static::lazy_static! { static ref MAP_ERR: RwLock io::Error>> = Default::default(); @@ -50,6 +50,12 @@ impl TraitCapturer for Capturer { } else { x })), + PixelProvider::RGB0(w, h, x) => Ok(Frame(if self.2 { + crate::common::rgba_to_i420(w as _, h as _, &x, &mut self.3); + &self.3[..] + } else { + x + })), PixelProvider::NONE => Err(std::io::ErrorKind::WouldBlock.into()), _ => Err(map_err("Invalid data")), } diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs index dacc265ff..61112bff7 100644 --- a/libs/scrap/src/common/x11.rs +++ b/libs/scrap/src/common/x11.rs @@ -3,7 +3,7 @@ use std::{io, ops, time::Duration}; pub struct Capturer(x11::Capturer); -pub const IS_CURSOR_EMBEDED: bool = false; +pub const IS_CURSOR_EMBEDDED: bool = false; impl Capturer { pub fn new(display: Display, yuv: bool) -> io::Result { diff --git a/libs/scrap/src/dxgi/mag.rs b/libs/scrap/src/dxgi/mag.rs index 78f14194c..0de86055e 100644 --- a/libs/scrap/src/dxgi/mag.rs +++ b/libs/scrap/src/dxgi/mag.rs @@ -339,7 +339,7 @@ impl CapturerMag { } // Register the host window class. See the MSDN documentation of the - // Magnification API for more infomation. + // Magnification API for more information. let wcex = WNDCLASSEXA { cbSize: size_of::() as _, style: 0, diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 6b60b256d..5829686b5 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -50,7 +50,7 @@ pub struct Capturer { rotated: Vec, gdi_capturer: Option, gdi_buffer: Vec, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl Capturer { diff --git a/libs/scrap/src/quartz/display.rs b/libs/scrap/src/quartz/display.rs index ff96b2c1c..47ace49db 100644 --- a/libs/scrap/src/quartz/display.rs +++ b/libs/scrap/src/quartz/display.rs @@ -13,6 +13,7 @@ impl Display { pub fn online() -> Result, CGError> { unsafe { + #[allow(invalid_value)] let mut arr: [u32; 16] = mem::MaybeUninit::uninit().assume_init(); let mut len: u32 = 0; diff --git a/libs/scrap/src/wayland/capturable.rs b/libs/scrap/src/wayland/capturable.rs index 05a5ec71d..61f80ecbf 100644 --- a/libs/scrap/src/wayland/capturable.rs +++ b/libs/scrap/src/wayland/capturable.rs @@ -4,6 +4,7 @@ use std::error::Error; pub enum PixelProvider<'a> { // 8 bits per color RGB(usize, usize, &'a [u8]), + RGB0(usize, usize, &'a [u8]), BGR0(usize, usize, &'a [u8]), // width, height, stride BGR0S(usize, usize, usize, &'a [u8]), @@ -14,6 +15,7 @@ impl<'a> PixelProvider<'a> { pub fn size(&self) -> (usize, usize) { match self { PixelProvider::RGB(w, h, _) => (*w, *h), + PixelProvider::RGB0(w, h, _) => (*w, *h), PixelProvider::BGR0(w, h, _) => (*w, *h), PixelProvider::BGR0S(w, h, _, _) => (*w, *h), PixelProvider::NONE => (0, 0), diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index a7b4c1357..c1c84f98e 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -117,12 +117,13 @@ impl Capturable for PipeWireCapturable { pub struct PipeWireRecorder { buffer: Option>, buffer_cropped: Vec, + pix_fmt: String, is_cropped: bool, pipeline: gst::Pipeline, appsink: AppSink, width: usize, height: usize, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl PipeWireRecorder { @@ -144,19 +145,27 @@ impl PipeWireRecorder { pipeline.add_many(&[&src, &sink])?; src.link(&sink)?; + let appsink = sink .dynamic_cast::() .map_err(|_| GStreamerError("Sink element is expected to be an appsink!".into()))?; - appsink.set_caps(Some(&gst::Caps::new_simple( + let mut caps = gst::Caps::new_empty(); + caps.merge_structure(gst::structure::Structure::new( "video/x-raw", &[("format", &"BGRx")], - ))); + )); + caps.merge_structure(gst::structure::Structure::new( + "video/x-raw", + &[("format", &"RGBx")], + )); + appsink.set_caps(Some(&caps)); pipeline.set_state(gst::State::Playing)?; Ok(Self { pipeline, appsink, buffer: None, + pix_fmt: "".into(), width: 0, height: 0, buffer_cropped: vec![], @@ -181,6 +190,11 @@ impl Recorder for PipeWireRecorder { let h: i32 = cap.get_value("height")?.get_some()?; let w = w as usize; let h = h as usize; + self.pix_fmt = cap + .get::<&str>("format")? + .ok_or("Failed to get pixel format")? + .to_string(); + let buf = sample .get_buffer_owned() .ok_or_else(|| GStreamerError("Failed to get owned buffer.".into()))?; @@ -241,15 +255,22 @@ impl Recorder for PipeWireRecorder { if self.buffer.is_none() { return Err(Box::new(GStreamerError("No buffer available!".into()))); } - Ok(PixelProvider::BGR0( - self.width, - self.height, - if self.is_cropped { - self.buffer_cropped.as_slice() - } else { - self.buffer.as_ref().unwrap().as_slice() - }, - )) + let buf = if self.is_cropped { + self.buffer_cropped.as_slice() + } else { + self.buffer + .as_ref() + .ok_or("Failed to get buffer as ref")? + .as_slice() + }; + match self.pix_fmt.as_str() { + "BGRx" => Ok(PixelProvider::BGR0(self.width, self.height, buf)), + "RGBx" => Ok(PixelProvider::RGB0(self.width, self.height, buf)), + _ => Err(Box::new(GStreamerError(format!( + "Unreachable! Unknown pix_fmt, {}", + &self.pix_fmt + )))), + } } } diff --git a/libs/scrap/src/x11/capturer.rs b/libs/scrap/src/x11/capturer.rs index ed424c35a..0dcfcfdab 100644 --- a/libs/scrap/src/x11/capturer.rs +++ b/libs/scrap/src/x11/capturer.rs @@ -14,7 +14,7 @@ pub struct Capturer { size: usize, use_yuv: bool, yuv: Vec, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl Capturer { diff --git a/libs/virtual_display/dylib/src/win10/IddController.c b/libs/virtual_display/dylib/src/win10/IddController.c index a30fa9d0a..c1faccfc2 100644 --- a/libs/virtual_display/dylib/src/win10/IddController.c +++ b/libs/virtual_display/dylib/src/win10/IddController.c @@ -66,7 +66,7 @@ const char* GetLastMsg() BOOL InstallUpdate(LPCWSTR fullInfPath, PBOOL rebootRequired) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); // UpdateDriverForPlugAndPlayDevicesW may return FALSE while driver was successfully installed... if (FALSE == UpdateDriverForPlugAndPlayDevicesW( @@ -96,7 +96,7 @@ BOOL InstallUpdate(LPCWSTR fullInfPath, PBOOL rebootRequired) BOOL Uninstall(LPCWSTR fullInfPath, PBOOL rebootRequired) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); if (FALSE == DiUninstallDriverW( NULL, @@ -122,7 +122,7 @@ BOOL Uninstall(LPCWSTR fullInfPath, PBOOL rebootRequired) BOOL IsDeviceCreated(PBOOL created) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); HDEVINFO hardwareDeviceInfo = SetupDiGetClassDevs( &GUID_DEVINTERFACE_IDD_DRIVER_DEVICE, @@ -181,7 +181,7 @@ BOOL IsDeviceCreated(PBOOL created) BOOL DeviceCreate(PHSWDEVICE hSwDevice) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); if (*hSwDevice != NULL) { @@ -221,7 +221,7 @@ BOOL DeviceCreate(PHSWDEVICE hSwDevice) SW_DEVICE_CREATE_INFO createInfo = { 0 }; PCWSTR description = L"RustDesk Idd Driver"; - // These match the Pnp id's in the inf file so OS will load the driver when the device is created + // These match the Pnp id's in the inf file so OS will load the driver when the device is created PCWSTR instanceId = L"RustDeskIddDriver"; PCWSTR hardwareIds = L"RustDeskIddDriver\0\0"; PCWSTR compatibleIds = L"RustDeskIddDriver\0\0"; @@ -274,7 +274,7 @@ BOOL DeviceCreate(PHSWDEVICE hSwDevice) VOID DeviceClose(HSWDEVICE hSwDevice) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); if (hSwDevice != INVALID_HANDLE_VALUE && hSwDevice != NULL) { @@ -284,7 +284,7 @@ VOID DeviceClose(HSWDEVICE hSwDevice) BOOL MonitorPlugIn(UINT index, UINT edid, INT retries) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); if (retries < 0) { @@ -359,7 +359,7 @@ BOOL MonitorPlugIn(UINT index, UINT edid, INT retries) BOOL MonitorPlugOut(UINT index) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); HANDLE hDevice = DeviceOpenHandle(); if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) @@ -400,7 +400,7 @@ BOOL MonitorPlugOut(UINT index) BOOL MonitorModesUpdate(UINT index, UINT modeCount, PMonitorMode modes) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); HANDLE hDevice = DeviceOpenHandle(); if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) @@ -721,7 +721,7 @@ Clean0: // https://stackoverflow.com/questions/67164846/createfile-fails-unless-i-disable-enable-my-device HANDLE DeviceOpenHandle() { - SetLastMsg("Sucess"); + SetLastMsg("Success"); // const int maxDevPathLen = 256; TCHAR devicePath[256] = { 0 }; diff --git a/libs/virtual_display/dylib/src/win10/IddController.h b/libs/virtual_display/dylib/src/win10/IddController.h index f92f72647..767d64798 100644 --- a/libs/virtual_display/dylib/src/win10/IddController.h +++ b/libs/virtual_display/dylib/src/win10/IddController.h @@ -14,7 +14,7 @@ extern "C" { * @param rebootRequired [out] Indicates whether a restart is required. * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg */ BOOL InstallUpdate(LPCTSTR fullInfPath, PBOOL rebootRequired); @@ -34,11 +34,11 @@ BOOL Uninstall(LPCTSTR fullInfPath, PBOOL rebootRequired); /** * @brief Check if RustDeskIddDriver device is created before. * The driver device(adapter) should be single instance. - * + * * @param created [out] Indicates whether the device is created before. * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg * */ @@ -47,12 +47,12 @@ BOOL IsDeviceCreated(PBOOL created); /** * @brief Create device. * Only one device should be created. - * If device is installed ealier, this function returns FALSE. - * + * If device is installed earlier, this function returns FALSE. + * * @param hSwDevice [out] Handler of software device, used by DeviceCreate(). Should be **NULL**. * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg * */ @@ -79,9 +79,9 @@ VOID DeviceClose(HSWDEVICE hSwDevice); * 1 means doing once and retry one time... * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg - * + * * @remark Plug in monitor may fail if device is created in a very short time. * System need some time to prepare the device. * @@ -94,7 +94,7 @@ BOOL MonitorPlugIn(UINT index, UINT edid, INT retries); * @param index [in] Monitor index, should be 0, 1, 2. * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg * */ @@ -133,9 +133,9 @@ const char* GetLastMsg(); * @brief Set if print error message when debug. * * @param b [in] TRUE to enable printing message. - * + * * @remark For now, no need to read evironment variable to check if should print. - * + * */ VOID SetPrintErrMsg(BOOL b); diff --git a/res/128x128.png b/res/128x128.png index cd35a0bc8..26cbf702c 100644 Binary files a/res/128x128.png and b/res/128x128.png differ diff --git a/res/128x128@2x.png b/res/128x128@2x.png index 3da699f1d..d6f8d20fa 100644 Binary files a/res/128x128@2x.png and b/res/128x128@2x.png differ diff --git a/res/32x32.png b/res/32x32.png index 21440d422..33dc80537 100644 Binary files a/res/32x32.png and b/res/32x32.png differ diff --git a/res/64x64.png b/res/64x64.png new file mode 100644 index 000000000..d93638e6e Binary files /dev/null and b/res/64x64.png differ diff --git a/res/gen_icon.sh b/res/gen_icon.sh index 40b67aa53..83252a6ae 100644 --- a/res/gen_icon.sh +++ b/res/gen_icon.sh @@ -3,5 +3,5 @@ for size in 16 32 64 128 256 512 1024; do #inkscape -z -o $size.png -w $size -h $size icon.svg >/dev/null 2>/dev/null convert icon.png -resize ${size}x${size} app_icon_$size.png done -# from ImageMagick +# from ImageMagick #/bin/rm 16.png 32.png 48.png 128.png 256.png diff --git a/res/icon-margin.png b/res/icon-margin.png index dfc650e9d..6449ec490 100644 Binary files a/res/icon-margin.png and b/res/icon-margin.png differ diff --git a/res/icon.png b/res/icon.png index 573165a06..823967c49 100644 Binary files a/res/icon.png and b/res/icon.png differ diff --git a/res/lang.py b/res/lang.py index 5aa6f4d15..37bbfb3b1 100644 --- a/res/lang.py +++ b/res/lang.py @@ -4,22 +4,22 @@ import os import glob import sys import csv - -def get_lang(lang): - out = {} - for ln in open('./src/lang/%s.rs'%lang): + +def get_lang(lang): + out = {} + for ln in open('./src/lang/%s.rs'%lang, encoding='utf8'): ln = ln.strip() if ln.startswith('("'): k, v = line_split(ln) out[k] = v - return out + return out def line_split(line): - toks = line.split('", "') + toks = line.split('", "') if len(toks) != 2: print(line) assert(0) - k = toks[0][2:] + k = toks[0][2:] v = toks[1][:-3] return k, v @@ -34,32 +34,33 @@ def main(): def expand(): - for fn in glob.glob('./src/lang/*'): - lang = os.path.basename(fn)[:-3] + for fn in glob.glob('./src/lang/*'): + lang = os.path.basename(fn)[:-3] if lang in ['en','cn']: continue print(lang) dict = get_lang(lang) - fw = open("./src/lang/%s.rs"%lang, "wt") - for line in open('./src/lang/cn.rs'): + fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8') + for line in open('./src/lang/cn.rs', encoding='utf8'): line_strip = line.strip() if line_strip.startswith('("'): k, v = line_split(line_strip) if k in dict: - line = line.replace(v, dict[k]) + # embrased with " to avoid empty v + line = line.replace('"%s"'%v, '"%s"'%dict[k]) else: line = line.replace(v, "") fw.write(line) else: fw.write(line) fw.close() - + def to_csv(): - for fn in glob.glob('./src/lang/*.rs'): - lang = os.path.basename(fn)[:-3] - csvfile = open('./src/lang/%s.csv'%lang, "wt") + for fn in glob.glob('./src/lang/*.rs'): + lang = os.path.basename(fn)[:-3] + csvfile = open('./src/lang/%s.csv'%lang, "wt", encoding='utf8') csvwriter = csv.writer(csvfile) - for line in open(fn): + for line in open(fn, encoding='utf8'): line_strip = line.strip() if line_strip.startswith('("'): k, v = line_split(line_strip) @@ -68,8 +69,8 @@ def to_csv(): def to_rs(lang): - csvfile = open('%s.csv'%lang, "rt") - fw = open("./src/lang/%s.rs"%lang, "wt") + csvfile = open('%s.csv'%lang, "rt", encoding='utf8') + fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8') fw.write('''lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ diff --git a/res/mac-tray-dark.png b/res/mac-tray-dark.png index 0ca3a9dd8..860f9fcf5 100644 Binary files a/res/mac-tray-dark.png and b/res/mac-tray-dark.png differ diff --git a/res/mac-tray-light.png b/res/mac-tray-light.png index c3e107410..f723d980e 100644 Binary files a/res/mac-tray-light.png and b/res/mac-tray-light.png differ diff --git a/src/cli.rs b/src/cli.rs index 59c356a5a..57d63d397 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,9 +1,12 @@ use crate::client::*; use hbb_common::{ config::PeerConfig, + config::READ_TIMEOUT, + futures::{SinkExt, StreamExt}, log, message_proto::*, protobuf::Message as _, + rendezvous_proto::ConnType, tokio::{self, sync::mpsc}, Stream, }; @@ -33,14 +36,18 @@ impl Session { .lc .write() .unwrap() - .initialize(id.to_owned(), false, true); + .initialize(id.to_owned(), ConnType::PORT_FORWARD); session } } #[async_trait] impl Interface for Session { - fn msgbox(&self, msgtype: &str, title: &str, text: &str) { + fn get_login_config_handler(&self) -> Arc> { + return self.lc.clone(); + } + + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { if msgtype == "input-password" { self.sender .send(Data::Login((self.password.clone(), true))) @@ -57,16 +64,15 @@ impl Interface for Session { } fn handle_login_error(&mut self, err: &str) -> bool { - self.lc.write().unwrap().handle_login_error(err, self) + handle_login_error(self.lc.clone(), err, self) } fn handle_peer_info(&mut self, pi: PeerInfo) { - let username = self.lc.read().unwrap().get_username(&pi); - self.lc.write().unwrap().handle_peer_info(username, pi); + self.lc.write().unwrap().handle_peer_info(&pi); } - async fn handle_hash(&mut self, hash: Hash, peer: &mut Stream) { - handle_hash(self.lc.clone(), hash, self, peer).await; + async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { + handle_hash(self.lc.clone(), &pass, hash, self, peer).await; } async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { @@ -82,6 +88,42 @@ impl Interface for Session { } } +#[tokio::main(flavor = "current_thread")] +pub async fn connect_test(id: &str, key: String, token: String) { + let (sender, mut receiver) = mpsc::unbounded_channel::(); + let handler = Session::new(&id, sender); + match crate::client::Client::start(id, &key, &token, ConnType::PORT_FORWARD, handler).await { + Err(err) => { + log::error!("Failed to connect {}: {}", &id, err); + } + Ok((mut stream, direct)) => { + log::info!("direct: {}", direct); + // rpassword::prompt_password("Input anything to exit").ok(); + loop { + tokio::select! { + res = hbb_common::timeout(READ_TIMEOUT, stream.next()) => match res { + Err(_) => { + log::error!("Timeout"); + break; + } + Ok(Some(Ok(bytes))) => { + let msg_in = Message::parse_from_bytes(&bytes).unwrap(); + match msg_in.union { + Some(message::Union::Hash(hash)) => { + log::info!("Got hash"); + break; + } + _ => {} + } + } + _ => {} + } + } + } + } + } +} + #[tokio::main(flavor = "current_thread")] pub async fn start_one_port_forward( id: String, @@ -95,9 +137,19 @@ pub async fn start_one_port_forward( crate::common::test_nat_type(); let (sender, mut receiver) = mpsc::unbounded_channel::(); let handler = Session::new(&id, sender); - handler.lc.write().unwrap().port_forward = (remote_host, remote_port); - if let Err(err) = - crate::port_forward::listen(handler.id.clone(), port, handler.clone(), receiver, &key, &token).await + if let Err(err) = crate::port_forward::listen( + handler.id.clone(), + handler.password.clone(), + port, + handler.clone(), + receiver, + &key, + &token, + handler.lc.clone(), + remote_host, + remote_port, + ) + .await { log::error!("Failed to listen on {}: {}", port, err); } diff --git a/src/client.rs b/src/client.rs index 1dd3021b2..635c8b661 100644 --- a/src/client.rs +++ b/src/client.rs @@ -6,8 +6,6 @@ use cpal::{ }; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] -use std::sync::atomic::Ordering; use std::{ collections::HashMap, net::SocketAddr, @@ -49,10 +47,7 @@ pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -use crate::{ - server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, - ui_session_interface::global_save_keyboard_mode, -}; +use crate::server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}; pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true); pub static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); @@ -172,12 +167,10 @@ impl Client { interface: impl Interface, ) -> ResultType<(Stream, bool)> { // to-do: remember the port for each peer, so that we can retry easier - let any_addr = Config::get_any_listen_addr(); - if crate::is_ip(peer) { + if hbb_common::is_ip_str(peer) { return Ok(( socket_client::connect_tcp( crate::check_port(peer, RELAY_PORT + 1), - any_addr, RENDEZVOUS_TIMEOUT, ) .await?, @@ -185,13 +178,12 @@ impl Client { )); } let (mut rendezvous_server, servers, contained) = crate::get_rendezvous_server(1_000).await; - let mut socket = - socket_client::connect_tcp(&*rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT).await; + let mut socket = socket_client::connect_tcp(&*rendezvous_server, RENDEZVOUS_TIMEOUT).await; debug_assert!(!servers.contains(&rendezvous_server)); if socket.is_err() && !servers.is_empty() { log::info!("try the other servers: {:?}", servers); for server in servers { - socket = socket_client::connect_tcp(&*server, any_addr, RENDEZVOUS_TIMEOUT).await; + socket = socket_client::connect_tcp(&*server, RENDEZVOUS_TIMEOUT).await; if socket.is_ok() { rendezvous_server = server; break; @@ -208,7 +200,7 @@ impl Client { let mut relay_server = "".to_owned(); let start = std::time::Instant::now(); - let mut peer_addr = any_addr; + let mut peer_addr = Config::get_any_listen_addr(true); let mut peer_nat_type = NatType::UNKNOWN_NAT; let my_nat_type = crate::get_nat_type(100).await; let mut is_local = false; @@ -269,9 +261,15 @@ impl Client { rr.relay_server ); signed_id_pk = rr.pk().into(); - let mut conn = - Self::create_relay(peer, rr.uuid, rr.relay_server, key, conn_type) - .await?; + let mut conn = Self::create_relay( + peer, + rr.uuid, + rr.relay_server, + key, + conn_type, + my_addr.is_ipv4(), + ) + .await?; Self::secure_connection( peer, signed_id_pk, @@ -378,7 +376,8 @@ impl Client { log::info!("peer address: {}, timeout: {}", peer, connect_timeout); let start = std::time::Instant::now(); // NOTICE: Socks5 is be used event in intranet. Which may be not a good way. - let mut conn = socket_client::connect_tcp(peer, local_addr, connect_timeout).await; + let mut conn = + socket_client::connect_tcp_local(peer, Some(local_addr), connect_timeout).await; let mut direct = !conn.is_err(); if interface.is_force_relay() || conn.is_err() { if !relay_server.is_empty() { @@ -422,7 +421,7 @@ impl Client { key: &str, conn: &mut Stream, direct: bool, - mut interface: impl Interface, + interface: impl Interface, ) -> ResultType<()> { let rs_pk = get_rs_pk(if key.is_empty() { hbb_common::config::RS_PUB_KEY @@ -511,16 +510,16 @@ impl Client { token: &str, conn_type: ConnType, ) -> ResultType { - let any_addr = Config::get_any_listen_addr(); let mut succeed = false; let mut uuid = "".to_owned(); + let mut ipv4 = true; for i in 1..=3 { // use different socket due to current hbbs implement requiring different nat address for each attempt - let mut socket = - socket_client::connect_tcp(rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT) - .await - .with_context(|| "Failed to connect to rendezvous server")?; + let mut socket = socket_client::connect_tcp(rendezvous_server, RENDEZVOUS_TIMEOUT) + .await + .with_context(|| "Failed to connect to rendezvous server")?; + ipv4 = socket.local_addr().is_ipv4(); let mut msg_out = RendezvousMessage::new(); uuid = Uuid::new_v4().to_string(); log::info!( @@ -555,7 +554,7 @@ impl Client { if !succeed { bail!("Timeout"); } - Self::create_relay(peer, uuid, relay_server, key, conn_type).await + Self::create_relay(peer, uuid, relay_server, key, conn_type, ipv4).await } /// Create a relay connection to the server. @@ -565,10 +564,10 @@ impl Client { relay_server: String, key: &str, conn_type: ConnType, + ipv4: bool, ) -> ResultType { let mut conn = socket_client::connect_tcp( - crate::check_port(relay_server, RELAY_PORT), - Config::get_any_listen_addr(), + socket_client::ipv4_to_ipv6(crate::check_port(relay_server, RELAY_PORT), ipv4), CONNECT_TIMEOUT, ) .await @@ -897,6 +896,8 @@ pub struct LoginConfigHandler { pub supported_encoding: Option<(bool, bool)>, pub restarting_remote_device: bool, pub force_relay: bool, + pub direct: Option, + pub received: bool, } impl Deref for LoginConfigHandler { @@ -934,6 +935,8 @@ impl LoginConfigHandler { self.supported_encoding = None; self.restarting_remote_device = false; self.force_relay = !self.get_option("force-always-relay").is_empty(); + self.direct = None; + self.received = false; } /// Check if the client should auto login. @@ -989,6 +992,17 @@ impl LoginConfigHandler { self.save_config(config); } + /// Save keyboard mode to the current config. + /// + /// # Arguments + /// + /// * `value` - The view style to be saved. + pub fn save_keyboard_mode(&mut self, value: String) { + let mut config = self.load_config(); + config.keyboard_mode = value; + self.save_config(config); + } + /// Save scroll style to the current config. /// /// # Arguments @@ -1332,32 +1346,6 @@ impl LoginConfigHandler { } } - /// Handle login error. - /// Return true if the password is wrong, return false if there's an actual error. - pub fn handle_login_error(&mut self, err: &str, interface: &impl Interface) -> bool { - if err == "Wrong Password" { - self.password = Default::default(); - interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); - true - } else if err == "No Password Access" { - self.password = Default::default(); - interface.msgbox( - "wait-remote-accept-nook", - "Prompt", - "Please wait for the remote side to accept your session request...", - "", - ); - true - } else { - if err.contains(SCRAP_X11_REQUIRED) { - interface.msgbox("error", "Login Error", err, SCRAP_X11_REF_URL); - } else { - interface.msgbox("error", "Login Error", err, ""); - } - false - } - } - /// Get user name. /// Return the name of the given peer. If the peer has no name, return the name in the config. /// @@ -1382,9 +1370,6 @@ impl LoginConfigHandler { if !pi.version.is_empty() { self.version = hbb_common::get_version_number(&pi.version); } - if hbb_common::get_version_number(&pi.version) < hbb_common::get_version_number("1.2.0") { - global_save_keyboard_mode("legacy".to_owned()); - } self.features = pi.features.clone().into_option(); let serde = PeerInfoSerde { username: pi.username.clone(), @@ -1407,6 +1392,14 @@ impl LoginConfigHandler { log::debug!("remove password of {}", self.id); } } + if config.keyboard_mode == "" { + if hbb_common::get_version_number(&pi.version) < hbb_common::get_version_number("1.2.0") + { + config.keyboard_mode = "legacy".to_string(); + } else { + config.keyboard_mode = "map".to_string(); + } + } self.conn_id = pi.conn_id; // no matter if change, for update file time self.save_config(config); @@ -1489,6 +1482,19 @@ impl LoginConfigHandler { msg_out.set_misc(misc); msg_out } + + pub fn set_force_relay(&mut self, direct: bool, received: bool) { + self.force_relay = false; + if direct && !received { + let errno = errno::errno().0; + log::info!("errno is {}", errno); + // TODO: check mac and ios + if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { + self.force_relay = true; + self.set_option("force-always-relay".to_owned(), "Y".to_owned()); + } + } + } } /// Media data. @@ -1715,6 +1721,36 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) { interface.send(Data::Message(msg_out)); } +/// Handle login error. +/// Return true if the password is wrong, return false if there's an actual error. +pub fn handle_login_error( + lc: Arc>, + err: &str, + interface: &impl Interface, +) -> bool { + if err == "Wrong Password" { + lc.write().unwrap().password = Default::default(); + interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); + true + } else if err == "No Password Access" { + lc.write().unwrap().password = Default::default(); + interface.msgbox( + "wait-remote-accept-nook", + "Prompt", + "Please wait for the remote side to accept your session request...", + "", + ); + true + } else { + if err.contains(SCRAP_X11_REQUIRED) { + interface.msgbox("error", "Login Error", err, SCRAP_X11_REF_URL); + } else { + interface.msgbox("error", "Login Error", err, ""); + } + false + } +} + /// Handle hash message sent by peer. /// Hash will be used for login. /// @@ -1803,17 +1839,23 @@ pub trait Interface: Send + Clone + 'static + Sized { fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str); fn handle_login_error(&mut self, err: &str) -> bool; fn handle_peer_info(&mut self, pi: PeerInfo); - fn set_force_relay(&mut self, direct: bool, received: bool); - fn is_file_transfer(&self) -> bool; - fn is_port_forward(&self) -> bool; - fn is_rdp(&self) -> bool; fn on_error(&self, err: &str) { self.msgbox("error", "Error", err, ""); } - fn is_force_relay(&self) -> bool; async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream); async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream); async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream); + + fn get_login_config_handler(&self) -> Arc>; + fn set_force_relay(&self, direct: bool, received: bool) { + self.get_login_config_handler() + .write() + .unwrap() + .set_force_relay(direct, received); + } + fn is_force_relay(&self) -> bool { + self.get_login_config_handler().read().unwrap().force_relay + } } /// Data used by the client interface. @@ -1979,11 +2021,10 @@ lazy_static::lazy_static! { /// * `title` - The title of the message. /// * `text` - The text of the message. #[inline] -pub fn check_if_retry(msgtype: &str, title: &str, text: &str) -> bool { +pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: bool) -> bool { msgtype == "error" && title == "Connection Error" - && (text.contains("10054") - || text.contains("104") + && ((text.contains("10054") || text.contains("104")) && retry_for_relay || (!text.to_lowercase().contains("offline") && !text.to_lowercase().contains("exist") && !text.to_lowercase().contains("handshake") @@ -1991,7 +2032,8 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str) -> bool { && !text.to_lowercase().contains("resolve") && !text.to_lowercase().contains("mismatch") && !text.to_lowercase().contains("manually") - && !text.to_lowercase().contains("not allowed"))) + && !text.to_lowercase().contains("not allowed") + && !text.to_lowercase().contains("reset by the peer"))) } #[inline] @@ -2024,8 +2066,3 @@ fn decode_id_pk(signed: &[u8], key: &sign::PublicKey) -> ResultType<(String, [u8 bail!("Wrong public length"); } } - -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] -pub fn disable_keyboard_listening() { - crate::ui_session_interface::KEYBOARD_HOOKED.store(true, Ordering::SeqCst); -} diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index b94177c51..2ecfca837 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -22,7 +22,7 @@ pub trait FileManager: Interface { #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] fn read_dir(&self, path: &str, include_hidden: bool) -> String { - use crate::flutter::make_fd_to_json; + use crate::common::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden) { Ok(fd) => make_fd_to_json(fd.id, fd.path, &fd.entries), Err(_) => "".into(), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 326857d3f..1f81dfa55 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -107,6 +107,7 @@ impl Remote { SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); SERVER_FILE_TRANSFER_ENABLED.store(true, Ordering::SeqCst); self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready + self.handler.set_connection_info(direct, false); // just build for now #[cfg(not(windows))] @@ -144,7 +145,10 @@ impl Remote { } Ok(ref bytes) => { last_recv_time = Instant::now(); - received = true; + if !received { + received = true; + self.handler.set_connection_info(direct, true); + } self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); if !self.handle_msg_from_peer(bytes, &mut peer).await { break @@ -986,7 +990,7 @@ impl Remote { self.video_sender.send(MediaData::Reset).ok(); if s.width > 0 && s.height > 0 { self.handler - .set_display(s.x, s.y, s.width, s.height, s.cursor_embeded); + .set_display(s.x, s.y, s.width, s.height, s.cursor_embedded); } } Some(misc::Union::CloseReason(c)) => { diff --git a/src/common.rs b/src/common.rs index ea02cf810..0be84e79f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -3,6 +3,14 @@ use std::{ sync::{Arc, Mutex}, }; +#[derive(Debug, Eq, PartialEq)] +pub enum GrabState { + Ready, + Run, + Wait, + Exit, +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use arboard::Clipboard as ClipboardContext; @@ -10,8 +18,7 @@ pub use arboard::Clipboard as ClipboardContext; use hbb_common::compress::decompress; use hbb_common::{ allow_err, - anyhow::bail, - compress::{compress as compress_func}, + compress::compress as compress_func, config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, get_version_number, log, message_proto::*, @@ -285,15 +292,7 @@ async fn test_nat_type_() -> ResultType { let start = std::time::Instant::now(); let (rendezvous_server, _, _) = get_rendezvous_server(1_000).await; let server1 = rendezvous_server; - let tmp: Vec<&str> = server1.split(":").collect(); - if tmp.len() != 2 { - bail!("Invalid server address: {}", server1); - } - let port: u16 = tmp[1].parse()?; - if port == 0 { - bail!("Invalid server address: {}", server1); - } - let server2 = format!("{}:{}", tmp[0], port - 1); + let server2 = crate::increase_port(&server1, -1); let mut msg_out = RendezvousMessage::new(); let serial = Config::get_serial(); msg_out.set_test_nat_request(TestNatRequest { @@ -302,21 +301,18 @@ async fn test_nat_type_() -> ResultType { }); let mut port1 = 0; let mut port2 = 0; - let server1 = socket_client::get_target_addr(&server1)?; - let server2 = socket_client::get_target_addr(&server2)?; - let mut addr = Config::get_any_listen_addr(); for i in 0..2 { let mut socket = socket_client::connect_tcp( - if i == 0 { - server1.clone() - } else { - server2.clone() - }, - addr, + if i == 0 { &*server1 } else { &*server2 }, RENDEZVOUS_TIMEOUT, ) .await?; - addr = socket.local_addr(); + if i == 0 { + Config::set_option( + "local-ip-addr".to_owned(), + socket.local_addr().ip().to_string(), + ); + } socket.send(&msg_out).await?; if let Some(Ok(bytes)) = socket.next_timeout(RENDEZVOUS_TIMEOUT).await { if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { @@ -339,7 +335,6 @@ async fn test_nat_type_() -> ResultType { break; } } - Config::set_option("local-ip-addr".to_owned(), addr.ip().to_string()); let ok = port1 > 0 && port2 > 0; if ok { let t = if port1 == port2 { @@ -360,13 +355,7 @@ pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec, boo let (mut a, mut b) = get_rendezvous_server_(ms_timeout).await; let mut b: Vec = b .drain(..) - .map(|x| { - if !x.contains(":") { - format!("{}:{}", x, config::RENDEZVOUS_PORT) - } else { - x - } - }) + .map(|x| socket_client::check_port(x, config::RENDEZVOUS_PORT)) .collect(); let c = if b.contains(&a) { b = b.drain(..).filter(|x| x != &a).collect(); @@ -416,7 +405,6 @@ async fn test_rendezvous_server_() { let tm = std::time::Instant::now(); if socket_client::connect_tcp( crate::check_port(&host, RENDEZVOUS_PORT), - Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT, ) .await @@ -473,11 +461,12 @@ pub fn username() -> String { #[inline] pub fn check_port(host: T, port: i32) -> String { - let host = host.to_string(); - if !host.contains(":") { - return format!("{}:{}", host, port); - } - return host; + hbb_common::socket_client::check_port(host, port) +} + +#[inline] +pub fn increase_port(host: T, offset: i32) -> String { + hbb_common::socket_client::increase_port(host, offset) } pub const POSTFIX_SERVICE: &'static str = "_service"; @@ -516,10 +505,9 @@ pub fn check_software_update() { async fn check_software_update_() -> hbb_common::ResultType<()> { sleep(3.).await; - let rendezvous_server = - socket_client::get_target_addr(&format!("rs-sg.rustdesk.com:{}", config::RENDEZVOUS_PORT))?; - let mut socket = - socket_client::new_udp(Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT).await?; + let rendezvous_server = format!("rs-sg.rustdesk.com:{}", config::RENDEZVOUS_PORT); + let (mut socket, rendezvous_server) = + socket_client::new_udp_for(&rendezvous_server, RENDEZVOUS_TIMEOUT).await?; let mut msg_out = RendezvousMessage::new(); msg_out.set_software_update(SoftwareUpdate { @@ -559,12 +547,6 @@ pub fn get_full_name() -> String { ) } -pub fn is_ip(id: &str) -> bool { - hbb_common::regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$") - .unwrap() - .is_match(id) -} - pub fn is_setup(name: &str) -> bool { name.to_lowercase().ends_with("install.exe") } @@ -595,29 +577,24 @@ pub fn get_api_server(api: String, custom: String) -> String { return lic.api.clone(); } } - let s = get_custom_rendezvous_server(custom); - if !s.is_empty() { - if s.contains(':') { - let tmp: Vec<&str> = s.split(":").collect(); - if tmp.len() == 2 { - let port: u16 = tmp[1].parse().unwrap_or(0); - if port > 2 { - return format!("http://{}:{}", tmp[0], port - 2); - } - } + let s0 = get_custom_rendezvous_server(custom); + if !s0.is_empty() { + let s = crate::increase_port(&s0, -2); + if s == s0 { + format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); } else { - return format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); + format!("http://{}", s); } } "https://admin.rustdesk.com".to_owned() } -pub fn get_audit_server(api: String, custom: String) -> String { +pub fn get_audit_server(api: String, custom: String, typ: String) -> String { let url = get_api_server(api, custom); if url.is_empty() || url.contains("rustdesk.com") { return "".to_owned(); } - format!("{}/api/audit", url) + format!("{}/api/audit/{}", url, typ) } pub async fn post_request(url: String, body: String, header: &str) -> ResultType { @@ -661,7 +638,7 @@ pub async fn post_request(url: String, body: String, header: &str) -> ResultType if !res.is_empty() { return Ok(res); } - bail!(String::from_utf8_lossy(&output.stderr).to_string()); + hbb_common::bail!(String::from_utf8_lossy(&output.stderr).to_string()); } } @@ -691,3 +668,27 @@ lazy_static::lazy_static! { lazy_static::lazy_static! { pub static ref IS_X11: Mutex = Mutex::new("x11" == hbb_common::platform::linux::get_display_server()); } + +pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> String { + 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)); + + let mut entries_out = vec![]; + for entry in entries { + let mut entry_map = serde_json::Map::new(); + entry_map.insert("entry_type".into(), json!(entry.entry_type.value())); + entry_map.insert("name".into(), json!(entry.name)); + entry_map.insert("size".into(), json!(entry.size)); + entry_map.insert("modified_time".into(), json!(entry.modified_time)); + entries_out.push(entry_map); + } + fd_json.insert("entries".into(), json!(entries_out)); + serde_json::to_string(&fd_json).unwrap_or("".into()) +} + +#[cfg(test)] +mod test_common { + use super::*; +} diff --git a/src/core_main.rs b/src/core_main.rs index b82f45f6a..9cb4483a1 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -38,6 +38,17 @@ pub fn core_main() -> Option> { } i += 1; } + #[cfg(target_os = "linux")] + #[cfg(feature = "flutter")] + { + crate::platform::linux::register_breakdown_handler(); + let (k, v) = ("LIBGL_ALWAYS_SOFTWARE", "true"); + if !hbb_common::config::Config::get_option("allow-always-software-render").is_empty() { + std::env::set_var(k, v); + } else { + std::env::remove_var(k); + } + } #[cfg(feature = "flutter")] if _is_flutter_connect { return core_main_invoke_new_connection(std::env::args()); @@ -173,7 +184,7 @@ pub fn core_main() -> Option> { crate::start_os_service(); return None; } else if args[0] == "--server" { - log::info!("start --server"); + log::info!("start --server with user {}", crate::username()); #[cfg(target_os = "windows")] { crate::start_server(true); @@ -182,6 +193,7 @@ pub fn core_main() -> Option> { #[cfg(target_os = "macos")] { std::thread::spawn(move || crate::start_server(true)); + crate::platform::macos::hide_dock(); crate::tray::make_tray(); return None; } @@ -238,6 +250,8 @@ pub fn core_main() -> Option> { #[cfg(feature = "flutter")] crate::flutter::connection_manager::start_listen_ipc_thread(); crate::ui_interface::start_option_status_sync(); + #[cfg(target_os = "macos")] + crate::platform::macos::hide_dock(); } } //_async_logger_holder.map(|x| x.flush()); @@ -312,7 +326,7 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option) -> ResultTy *session.event_stream.write().unwrap() = Some(event_stream); let session = session.clone(); std::thread::spawn(move || { - // if flutter : disable keyboard listen - crate::client::disable_keyboard_listening(); io_loop(session); }); Ok(()) @@ -545,24 +545,6 @@ pub fn get_session_id(id: String) -> String { }; } -pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> String { - let mut fd_json = serde_json::Map::new(); - fd_json.insert("id".into(), json!(id)); - fd_json.insert("path".into(), json!(path)); - - let mut entries_out = vec![]; - for entry in entries { - let mut entry_map = serde_json::Map::new(); - entry_map.insert("entry_type".into(), json!(entry.entry_type.value())); - entry_map.insert("name".into(), json!(entry.name)); - entry_map.insert("size".into(), json!(entry.size)); - entry_map.insert("modified_time".into(), json!(entry.modified_time)); - entries_out.push(entry_map); - } - fd_json.insert("entries".into(), json!(entries_out)); - serde_json::to_string(&fd_json).unwrap_or("".into()) -} - pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> String { let mut m = serde_json::Map::new(); m.insert("id".into(), json!(id)); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 1250f7e19..92f1e0606 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -17,11 +17,10 @@ use hbb_common::{ use crate::flutter::{self, SESSIONS}; use crate::ui_interface::{self, *}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::ui_session_interface::CUR_SESSION; use crate::{ client::file_trait::FileManager, - flutter::{make_fd_to_json, session_add, session_start_}, + common::make_fd_to_json, + flutter::{session_add, session_start_}, }; fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); @@ -183,6 +182,14 @@ pub fn set_local_flutter_config(k: String, v: String) { ui_interface::set_local_flutter_config(k, v); } +pub fn get_local_kb_layout_type() -> SyncReturn { + SyncReturn(ui_interface::get_kb_layout_type()) +} + +pub fn set_local_kb_layout_type(kb_layout_type: String) { + ui_interface::set_kb_layout_type(kb_layout_type) +} + pub fn session_get_view_style(id: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_view_style()) @@ -225,6 +232,20 @@ pub fn session_set_image_quality(id: String, value: String) { } } +pub fn session_get_keyboard_mode(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_keyboard_mode()) + } else { + None + } +} + +pub fn session_set_keyboard_mode(id: String, value: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_keyboard_mode(value); + } +} + pub fn session_get_custom_image_quality(id: String) -> Option> { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_custom_image_quality()) @@ -279,10 +300,9 @@ pub fn session_enter_or_leave(id: String, enter: bool) { #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Some(session) = SESSIONS.read().unwrap().get(&id) { if enter { - *CUR_SESSION.lock().unwrap() = Some(session.clone()); + crate::keyboard::set_cur_session(session.clone()); session.enter(); } else { - *CUR_SESSION.lock().unwrap() = None; session.leave(); } } @@ -299,12 +319,14 @@ pub fn session_input_key( command: bool, ) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { + // #[cfg(any(target_os = "android", target_os = "ios"))] session.input_key(&name, down, press, alt, ctrl, shift, command); } } pub fn session_input_string(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { + // #[cfg(any(target_os = "android", target_os = "ios"))] session.input_string(&value); } } @@ -329,19 +351,6 @@ pub fn session_get_peer_option(id: String, name: String) -> String { "".to_string() } -pub fn session_get_keyboard_name(id: String) -> String { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - return session.get_keyboard_mode(); - } - "legacy".to_string() -} - -pub fn session_set_keyboard_mode(id: String, keyboard_mode: String) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.save_keyboard_mode(keyboard_mode); - } -} - pub fn session_input_os_password(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.input_os_password(value, true); @@ -876,9 +885,11 @@ pub fn session_send_mouse(id: String, msg: String) { } if let Some(buttons) = m.get("buttons") { mask |= match buttons.as_str() { - "left" => 1, - "right" => 2, - "wheel" => 4, + "left" => 0x01, + "right" => 0x02, + "wheel" => 0x04, + "back" => 0x08, + "forward" => 0x10, _ => 0, } << 3; } @@ -894,9 +905,9 @@ pub fn session_restart_remote_device(id: String) { } } -pub fn session_get_audit_server_sync(id: String) -> SyncReturn { +pub fn session_get_audit_server_sync(id: String, typ: String) -> SyncReturn { let res = if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.get_audit_server() + session.get_audit_server(typ) } else { "".to_owned() }; @@ -1083,8 +1094,7 @@ pub fn main_is_installed() -> SyncReturn { } pub fn main_start_grab_keyboard() { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - crate::ui_session_interface::global_grab_keyboard(); + crate::keyboard::client::start_grab_loop(); } pub fn main_is_installed_lower_version() -> SyncReturn { @@ -1103,6 +1113,10 @@ pub fn main_is_can_screen_recording(prompt: bool) -> SyncReturn { SyncReturn(is_can_screen_recording(prompt)) } +pub fn main_is_can_input_monitoring(prompt: bool) -> SyncReturn { + SyncReturn(is_can_input_monitoring(prompt)) +} + pub fn main_is_share_rdp() -> SyncReturn { SyncReturn(is_share_rdp()) } diff --git a/src/ipc.rs b/src/ipc.rs index 478094cf2..c562225b4 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -78,7 +78,7 @@ pub enum FS { WriteError { id: i32, file_num: i32, - err: String + err: String, }, WriteOffset { id: i32, @@ -111,7 +111,7 @@ pub enum DataKeyboardResponse { GetKeyState(bool), } -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] pub enum DataMouse { @@ -195,11 +195,11 @@ pub enum Data { ClipboardFileEnabled(bool), PrivacyModeState((i32, PrivacyModeState)), TestRendezvousServer, - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Keyboard(DataKeyboard), - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] KeyboardResponse(DataKeyboardResponse), - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Mouse(DataMouse), Control(DataControl), Theme(String), @@ -544,7 +544,7 @@ async fn check_pid(postfix: &str) { } } } - hbb_common::allow_err!(std::fs::remove_file(&Config::ipc_path(postfix))); + std::fs::remove_file(&Config::ipc_path(postfix)).ok(); } #[inline] diff --git a/src/keyboard.rs b/src/keyboard.rs new file mode 100644 index 000000000..9fa53757f --- /dev/null +++ b/src/keyboard.rs @@ -0,0 +1,655 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::client::get_key_state; +use crate::common::GrabState; +#[cfg(feature = "flutter")] +use crate::flutter::FlutterHandler; +#[cfg(not(any(feature = "flutter", feature = "cli")))] +use crate::ui::remote::SciterHandler; +use crate::ui_session_interface::Session; +use hbb_common::{log, message_proto::*}; +use rdev::{Event, EventType, Key}; +#[cfg(any(target_os = "windows", target_os = "macos"))] +use std::sync::atomic::{AtomicBool, Ordering}; +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, Mutex}, + time::SystemTime, +}; + +#[cfg(windows)] +static mut IS_ALT_GR: bool = false; + +#[cfg(any(target_os = "windows", target_os = "macos"))] +static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); + +#[cfg(feature = "flutter")] +lazy_static::lazy_static! { + static ref CUR_SESSION: Arc>>> = Default::default(); +} + +#[cfg(not(any(feature = "flutter", feature = "cli")))] +lazy_static::lazy_static! { + static ref CUR_SESSION: Arc>>> = Default::default(); +} + +lazy_static::lazy_static! { + static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); + static ref MODIFIERS_STATE: Mutex> = { + let mut m = HashMap::new(); + m.insert(Key::ShiftLeft, false); + m.insert(Key::ShiftRight, false); + m.insert(Key::ControlLeft, false); + m.insert(Key::ControlRight, false); + m.insert(Key::Alt, false); + m.insert(Key::AltGr, false); + m.insert(Key::MetaLeft, false); + m.insert(Key::MetaRight, false); + Mutex::new(m) + }; +} + +#[cfg(feature = "flutter")] +pub fn set_cur_session(session: Session) { + *CUR_SESSION.lock().unwrap() = Some(session); +} + +#[cfg(not(any(feature = "flutter", feature = "cli")))] +pub fn set_cur_session(session: Session) { + *CUR_SESSION.lock().unwrap() = Some(session); +} + +pub mod client { + use super::*; + + pub fn get_keyboard_mode() -> String { + #[cfg(not(feature = "cli"))] + if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { + return handler.get_keyboard_mode(); + } + "legacy".to_string() + } + + pub fn start_grab_loop() { + super::start_grab_loop(); + } + + pub fn change_grab_status(state: GrabState) { + match state { + GrabState::Ready => {} + GrabState::Run => { + #[cfg(any(target_os = "windows", target_os = "macos"))] + KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); + + #[cfg(target_os = "linux")] + rdev::enable_grab(); + } + GrabState::Wait => { + release_remote_keys(); + + #[cfg(any(target_os = "windows", target_os = "macos"))] + KEYBOARD_HOOKED.swap(false, Ordering::SeqCst); + + #[cfg(target_os = "linux")] + rdev::disable_grab(); + } + GrabState::Exit => { + #[cfg(target_os = "linux")] + rdev::exit_grab_listen(); + } + } + } + + pub fn process_event(event: &Event) { + if is_long_press(&event) { + return; + } + if let Some(key_event) = event_to_key_event(&event) { + send_key_event(&key_event); + } + } + + pub fn get_modifiers_state( + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) -> (bool, bool, bool, bool) { + let modifiers_lock = MODIFIERS_STATE.lock().unwrap(); + let ctrl = *modifiers_lock.get(&Key::ControlLeft).unwrap() + || *modifiers_lock.get(&Key::ControlRight).unwrap() + || ctrl; + let shift = *modifiers_lock.get(&Key::ShiftLeft).unwrap() + || *modifiers_lock.get(&Key::ShiftRight).unwrap() + || shift; + let command = *modifiers_lock.get(&Key::MetaLeft).unwrap() + || *modifiers_lock.get(&Key::MetaRight).unwrap() + || command; + let alt = *modifiers_lock.get(&Key::Alt).unwrap() + || *modifiers_lock.get(&Key::AltGr).unwrap() + || alt; + + (alt, ctrl, shift, command) + } + + pub fn legacy_modifiers( + key_event: &mut KeyEvent, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + if alt + && !crate::is_control_key(&key_event, &ControlKey::Alt) + && !crate::is_control_key(&key_event, &ControlKey::RAlt) + { + key_event.modifiers.push(ControlKey::Alt.into()); + } + if shift + && !crate::is_control_key(&key_event, &ControlKey::Shift) + && !crate::is_control_key(&key_event, &ControlKey::RShift) + { + key_event.modifiers.push(ControlKey::Shift.into()); + } + if ctrl + && !crate::is_control_key(&key_event, &ControlKey::Control) + && !crate::is_control_key(&key_event, &ControlKey::RControl) + { + key_event.modifiers.push(ControlKey::Control.into()); + } + if command + && !crate::is_control_key(&key_event, &ControlKey::Meta) + && !crate::is_control_key(&key_event, &ControlKey::RWin) + { + key_event.modifiers.push(ControlKey::Meta.into()); + } + } + + pub fn lock_screen() { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::LockScreen); + key_event.down = true; + key_event.mode = KeyboardMode::Legacy.into(); + send_key_event(&key_event); + } + + pub fn ctrl_alt_del() { + let mut key_event = KeyEvent::new(); + if get_peer_platform() == "Windows" { + key_event.set_control_key(ControlKey::CtrlAltDel); + key_event.down = true; + } else { + key_event.set_control_key(ControlKey::Delete); + legacy_modifiers(&mut key_event, true, true, false, false); + key_event.press = true; + } + key_event.mode = KeyboardMode::Legacy.into(); + send_key_event(&key_event); + } +} + +pub fn start_grab_loop() { + #[cfg(any(target_os = "windows", target_os = "macos"))] + std::thread::spawn(move || { + let try_handle_keyboard = move |event: Event, key: Key, is_press: bool| -> Option { + // fix #2211:CAPS LOCK don't work + if key == Key::CapsLock || key == Key::NumLock { + return Some(event); + } + if KEYBOARD_HOOKED.load(Ordering::SeqCst) { + client::process_event(&event); + if is_press { + return None; + } else { + return Some(event); + } + } else { + return Some(event); + } + }; + let func = move |event: Event| match event.event_type { + EventType::KeyPress(key) => try_handle_keyboard(event, key, true), + EventType::KeyRelease(key) => try_handle_keyboard(event, key, false), + _ => Some(event), + }; + if let Err(error) = rdev::grab(func) { + log::error!("rdev Error: {:?}", error) + } + }); + + #[cfg(target_os = "linux")] + if let Err(err) = rdev::start_grab_listen(move |event: Event| match event.event_type { + EventType::KeyPress(key) | EventType::KeyRelease(key) => { + if let Key::Unknown(keycode) = key { + log::error!("rdev get unknown key, keycode is : {:?}", keycode); + } else { + client::process_event(&event); + } + None + } + _ => Some(event), + }) { + log::error!("Failed to init rdev grab thread: {:?}", err); + }; +} + +pub fn is_long_press(event: &Event) -> bool { + let keys = MODIFIERS_STATE.lock().unwrap(); + match event.event_type { + EventType::KeyPress(k) => { + if let Some(&state) = keys.get(&k) { + if state == true { + return true; + } + } + } + _ => {} + }; + return false; +} + +pub fn release_remote_keys() { + // todo!: client quit suddenly, how to release keys? + let to_release = TO_RELEASE.lock().unwrap().clone(); + TO_RELEASE.lock().unwrap().clear(); + for key in to_release { + let event_type = EventType::KeyRelease(key); + let event = event_type_to_event(event_type); + client::process_event(&event); + } +} + +pub fn get_keyboard_mode_enum() -> KeyboardMode { + match client::get_keyboard_mode().as_str() { + "map" => KeyboardMode::Map, + "translate" => KeyboardMode::Translate, + _ => KeyboardMode::Legacy, + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn add_numlock_capslock_status(key_event: &mut KeyEvent) { + if get_key_state(enigo::Key::CapsLock) { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if get_key_state(enigo::Key::NumLock) { + key_event.modifiers.push(ControlKey::NumLock.into()); + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn convert_numpad_keys(key: Key) -> Key { + if get_key_state(enigo::Key::NumLock) { + return key; + } + match key { + Key::Kp0 => Key::Insert, + Key::KpDecimal => Key::Delete, + Key::Kp1 => Key::End, + Key::Kp2 => Key::DownArrow, + Key::Kp3 => Key::PageDown, + Key::Kp4 => Key::LeftArrow, + Key::Kp5 => Key::Clear, + Key::Kp6 => Key::RightArrow, + Key::Kp7 => Key::Home, + Key::Kp8 => Key::UpArrow, + Key::Kp9 => Key::PageUp, + _ => key, + } +} + +fn update_modifiers_state(event: &Event) { + // for mouse + let mut keys = MODIFIERS_STATE.lock().unwrap(); + match event.event_type { + EventType::KeyPress(k) => { + if keys.contains_key(&k) { + keys.insert(k, true); + } + } + EventType::KeyRelease(k) => { + if keys.contains_key(&k) { + keys.insert(k, false); + } + } + _ => {} + }; +} + +pub fn event_to_key_event(event: &Event) -> Option { + let mut key_event = KeyEvent::new(); + update_modifiers_state(event); + + match event.event_type { + EventType::KeyPress(key) => { + TO_RELEASE.lock().unwrap().insert(key); + } + EventType::KeyRelease(key) => { + TO_RELEASE.lock().unwrap().remove(&key); + } + _ => {} + } + + let keyboard_mode = get_keyboard_mode_enum(); + key_event.mode = keyboard_mode.into(); + let mut key_event = match keyboard_mode { + KeyboardMode::Map => map_keyboard_mode(event, key_event)?, + KeyboardMode::Translate => translate_keyboard_mode(event, key_event)?, + _ => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + legacy_keyboard_mode(event, key_event)? + } + #[cfg(any(target_os = "android", target_os = "ios"))] + { + None? + } + } + }; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + add_numlock_capslock_status(&mut key_event); + + return Some(key_event); +} + +pub fn event_type_to_event(event_type: EventType) -> Event { + Event { + event_type, + time: SystemTime::now(), + name: None, + code: 0, + scan_code: 0, + } +} + +pub fn send_key_event(key_event: &KeyEvent) { + #[cfg(not(feature = "cli"))] + if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { + handler.send_key_event(key_event); + } +} + +pub fn get_peer_platform() -> String { + #[cfg(not(feature = "cli"))] + if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { + return handler.peer_platform(); + } + "Windows".to_string() +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { + // legacy mode(0): Generate characters locally, look for keycode on other side. + let (mut key, down_or_up) = match event.event_type { + EventType::KeyPress(key) => (key, true), + EventType::KeyRelease(key) => (key, false), + _ => { + return None; + } + }; + + let peer = get_peer_platform(); + let is_win = peer == "Windows"; + if is_win { + key = convert_numpad_keys(key); + } + + let alt = get_key_state(enigo::Key::Alt); + #[cfg(windows)] + let ctrl = { + let mut tmp = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); + unsafe { + if IS_ALT_GR { + if alt || key == Key::AltGr { + if tmp { + tmp = false; + } + } else { + IS_ALT_GR = false; + } + } + } + tmp + }; + #[cfg(not(windows))] + let ctrl = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); + let shift = get_key_state(enigo::Key::Shift) || get_key_state(enigo::Key::RightShift); + #[cfg(windows)] + let command = crate::platform::windows::get_win_key_state(); + #[cfg(not(windows))] + let command = get_key_state(enigo::Key::Meta); + let control_key = match key { + Key::Alt => Some(ControlKey::Alt), + Key::AltGr => Some(ControlKey::RAlt), + Key::Backspace => Some(ControlKey::Backspace), + Key::ControlLeft => { + // when pressing AltGr, an extra VK_LCONTROL with a special + // scancode with bit 9 set is sent, let's ignore this. + #[cfg(windows)] + if (event.scan_code >> 8) == 0xE0 { + unsafe { + IS_ALT_GR = true; + } + return None; + } + Some(ControlKey::Control) + } + Key::ControlRight => Some(ControlKey::RControl), + Key::DownArrow => Some(ControlKey::DownArrow), + Key::Escape => Some(ControlKey::Escape), + Key::F1 => Some(ControlKey::F1), + Key::F10 => Some(ControlKey::F10), + Key::F11 => Some(ControlKey::F11), + Key::F12 => Some(ControlKey::F12), + Key::F2 => Some(ControlKey::F2), + Key::F3 => Some(ControlKey::F3), + Key::F4 => Some(ControlKey::F4), + Key::F5 => Some(ControlKey::F5), + Key::F6 => Some(ControlKey::F6), + Key::F7 => Some(ControlKey::F7), + Key::F8 => Some(ControlKey::F8), + Key::F9 => Some(ControlKey::F9), + Key::LeftArrow => Some(ControlKey::LeftArrow), + Key::MetaLeft => Some(ControlKey::Meta), + Key::MetaRight => Some(ControlKey::RWin), + Key::Return => Some(ControlKey::Return), + Key::RightArrow => Some(ControlKey::RightArrow), + Key::ShiftLeft => Some(ControlKey::Shift), + Key::ShiftRight => Some(ControlKey::RShift), + Key::Space => Some(ControlKey::Space), + Key::Tab => Some(ControlKey::Tab), + Key::UpArrow => Some(ControlKey::UpArrow), + Key::Delete => { + if is_win && ctrl && alt { + client::ctrl_alt_del(); + return None; + } + Some(ControlKey::Delete) + } + Key::Apps => Some(ControlKey::Apps), + Key::Cancel => Some(ControlKey::Cancel), + Key::Clear => Some(ControlKey::Clear), + Key::Kana => Some(ControlKey::Kana), + Key::Hangul => Some(ControlKey::Hangul), + Key::Junja => Some(ControlKey::Junja), + Key::Final => Some(ControlKey::Final), + Key::Hanja => Some(ControlKey::Hanja), + Key::Hanji => Some(ControlKey::Hanja), + Key::Convert => Some(ControlKey::Convert), + Key::Print => Some(ControlKey::Print), + Key::Select => Some(ControlKey::Select), + Key::Execute => Some(ControlKey::Execute), + Key::PrintScreen => Some(ControlKey::Snapshot), + Key::Help => Some(ControlKey::Help), + Key::Sleep => Some(ControlKey::Sleep), + Key::Separator => Some(ControlKey::Separator), + Key::KpReturn => Some(ControlKey::NumpadEnter), + Key::Kp0 => Some(ControlKey::Numpad0), + Key::Kp1 => Some(ControlKey::Numpad1), + Key::Kp2 => Some(ControlKey::Numpad2), + Key::Kp3 => Some(ControlKey::Numpad3), + Key::Kp4 => Some(ControlKey::Numpad4), + Key::Kp5 => Some(ControlKey::Numpad5), + Key::Kp6 => Some(ControlKey::Numpad6), + Key::Kp7 => Some(ControlKey::Numpad7), + Key::Kp8 => Some(ControlKey::Numpad8), + Key::Kp9 => Some(ControlKey::Numpad9), + Key::KpDivide => Some(ControlKey::Divide), + Key::KpMultiply => Some(ControlKey::Multiply), + Key::KpDecimal => Some(ControlKey::Decimal), + Key::KpMinus => Some(ControlKey::Subtract), + Key::KpPlus => Some(ControlKey::Add), + Key::CapsLock | Key::NumLock | Key::ScrollLock => { + return None; + } + Key::Home => Some(ControlKey::Home), + Key::End => Some(ControlKey::End), + Key::Insert => Some(ControlKey::Insert), + Key::PageUp => Some(ControlKey::PageUp), + Key::PageDown => Some(ControlKey::PageDown), + Key::Pause => Some(ControlKey::Pause), + _ => None, + }; + if let Some(k) = control_key { + key_event.set_control_key(k); + } else { + let mut chr = match event.name { + Some(ref s) => { + if s.len() <= 2 { + // exclude chinese characters + s.chars().next().unwrap_or('\0') + } else { + '\0' + } + } + _ => '\0', + }; + if chr == '·' { + // special for Chinese + chr = '`'; + } + if chr == '\0' { + chr = match key { + Key::Num1 => '1', + Key::Num2 => '2', + Key::Num3 => '3', + Key::Num4 => '4', + Key::Num5 => '5', + Key::Num6 => '6', + Key::Num7 => '7', + Key::Num8 => '8', + Key::Num9 => '9', + Key::Num0 => '0', + Key::KeyA => 'a', + Key::KeyB => 'b', + Key::KeyC => 'c', + Key::KeyD => 'd', + Key::KeyE => 'e', + Key::KeyF => 'f', + Key::KeyG => 'g', + Key::KeyH => 'h', + Key::KeyI => 'i', + Key::KeyJ => 'j', + Key::KeyK => 'k', + Key::KeyL => 'l', + Key::KeyM => 'm', + Key::KeyN => 'n', + Key::KeyO => 'o', + Key::KeyP => 'p', + Key::KeyQ => 'q', + Key::KeyR => 'r', + Key::KeyS => 's', + Key::KeyT => 't', + Key::KeyU => 'u', + Key::KeyV => 'v', + Key::KeyW => 'w', + Key::KeyX => 'x', + Key::KeyY => 'y', + Key::KeyZ => 'z', + Key::Comma => ',', + Key::Dot => '.', + Key::SemiColon => ';', + Key::Quote => '\'', + Key::LeftBracket => '[', + Key::RightBracket => ']', + Key::Slash => '/', + Key::BackSlash => '\\', + Key::Minus => '-', + Key::Equal => '=', + Key::BackQuote => '`', + _ => '\0', + } + } + if chr != '\0' { + if chr == 'l' && is_win && command { + client::lock_screen(); + return None; + } + key_event.set_chr(chr as _); + } else { + log::error!("Unknown key {:?}", &event); + return None; + } + } + let (alt, ctrl, shift, command) = client::get_modifiers_state(alt, ctrl, shift, command); + client::legacy_modifiers(&mut key_event, alt, ctrl, shift, command); + + if down_or_up == true { + key_event.down = true; + } + Some(key_event) +} + +pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { + match event.event_type { + EventType::KeyPress(..) => { + key_event.down = true; + } + EventType::KeyRelease(..) => { + key_event.down = false; + } + _ => return None, + }; + + let mut peer = get_peer_platform().to_lowercase(); + peer.retain(|c| !c.is_whitespace()); + + #[cfg(target_os = "windows")] + let keycode = match peer.as_str() { + "windows" => event.scan_code, + "macos" => { + if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { + rdev::win_scancode_to_macos_iso_code(event.scan_code)? + } else { + rdev::win_scancode_to_macos_code(event.scan_code)? + } + } + _ => rdev::win_scancode_to_linux_code(event.scan_code)?, + }; + #[cfg(target_os = "macos")] + let keycode = match peer.as_str() { + "windows" => rdev::macos_code_to_win_scancode(event.code as _)?, + "macos" => event.code as _, + _ => rdev::macos_code_to_linux_code(event.code as _)?, + }; + #[cfg(target_os = "linux")] + let keycode = match peer.as_str() { + "windows" => rdev::linux_code_to_win_scancode(event.code as _)?, + "macos" => { + if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { + rdev::linux_code_to_macos_iso_code(event.code as _)? + } else { + rdev::linux_code_to_macos_code(event.code as _)? + } + } + _ => event.code as _, + }; + #[cfg(any(target_os = "android", target_os = "ios"))] + let keycode = 0; + + key_event.set_chr(keycode); + Some(key_event) +} + +pub fn translate_keyboard_mode(_event: &Event, mut _key_event: KeyEvent) -> Option { + None +} diff --git a/src/lang.rs b/src/lang.rs index cec9801c2..65505cd70 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -28,6 +28,9 @@ mod ca; mod gr; mod sv; mod sq; +mod sr; +mod th; +mod sl; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -59,6 +62,9 @@ lazy_static::lazy_static! { ("gr", "Ελληνικά"), ("sv", "Svenska"), ("sq", "Shqip"), + ("sr", "Srpski"), + ("th", "ภาษาไทย"), + ("sl", "Slovenščina"), ]); } @@ -114,6 +120,9 @@ pub fn translate_locale(name: String, locale: &str) -> String { "gr" => gr::T.deref(), "sv" => sv::T.deref(), "sq" => sq::T.deref(), + "sr" => sr::T.deref(), + "th" => th::T.deref(), + "sl" => sl::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 70190729c..9224d231a 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Canviar ID"), ("Website", "Lloc web"), ("About", "Sobre"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada d'àudio"), ("Enhancements", "Millores"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Connecta sempre a través de relay"), ("whitelist_tip", ""), ("Login", "Inicia sessió"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Sortir"), ("Tags", ""), ("Search ID", "Cerca ID"), @@ -331,21 +338,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale original", "Escala original"), ("Scale adaptive", "Escala adaptativa"), ("General", ""), - ("Security", "Seguritat"), - ("Account", "Compte"), + ("Security", "Seguretat"), ("Theme", "Tema"), ("Dark Theme", "Tema Fosc"), ("Dark", "Fosc"), ("Light", "Clar"), ("Follow System", "Tema del sistema"), ("Enable hardware codec", "Habilitar còdec per hardware"), - ("Unlock Security Settings", "Desbloquejar ajustaments de seguritat"), + ("Unlock Security Settings", "Desbloquejar ajustaments de seguretat"), ("Enable Audio", "Habilitar àudio"), ("Unlock Network Settings", "Desbloquejar Ajustaments de Xarxa"), ("Server", "Servidor"), ("Direct IP Access", "Accés IP Directe"), ("Proxy", ""), - ("Port", ""), ("Apply", "Aplicar"), ("Disconnect all devices?", "Desconnectar tots els dispositius?"), ("Clear", "Netejar"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Altre"), ("Confirm before closing multiple tabs", "Confirmar abans de tancar múltiples pestanyes"), ("Keyboard Settings", "Ajustaments de teclat"), - ("Custom", "Personalitzat"), ("Full Access", "Acces complet"), ("Screen Share", "Compartir pantalla"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o una versió superior."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index daa2af065..a486128b7 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -7,7 +7,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Password", "密码"), ("Ready", "就绪"), ("Established", "已建立"), - ("connecting_status", "正在接入RustDesk网络..."), + ("connecting_status", "正在接入 RustDesk 网络..."), ("Enable Service", "允许服务"), ("Start Service", "启动服务"), ("Service is running", "服务正在运行"), @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "改变ID"), ("Website", "网站"), ("About", "关于"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "静音"), ("Audio Input", "音频输入"), ("Enhancements", "增强功能"), @@ -116,7 +118,7 @@ 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", "禁止剪贴板"), @@ -136,13 +138,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "无法建立直接连接"), ("Set Password", "设置密码"), ("OS Password", "操作系统密码"), - ("install_tip", "你正在运行未安装版本,由于UAC限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将RustDesk安装到系统,从而规避上述问题。"), + ("install_tip", "你正在运行未安装版本,由于UAC限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将 RustDesk 安装到系统,从而规避上述问题。"), ("Click to upgrade", "点击这里升级"), ("Click to download", "点击这里下载"), ("Click to update", "点击这里更新"), ("Configure", "配置"), - ("config_acc", "为了能够远程控制你的桌面, 请给予RustDesk\"辅助功能\" 权限。"), - ("config_screen", "为了能够远程访问你的桌面, 请给予RustDesk\"屏幕录制\" 权限。"), + ("config_acc", "为了能够远程控制你的桌面, 请给予 RustDesk \"辅助功能\" 权限。"), + ("config_screen", "为了能够远程访问你的桌面, 请给予 RustDesk \"屏幕录制\" 权限。"), ("Installing ...", "安装 ..."), ("Install", "安装"), ("Installation", "安装"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "强制走中继连接"), ("whitelist_tip", "只有白名单里的ip才能访问我"), ("Login", "登录"), + ("Verify", "验证"), + ("Remember me", "记住我"), + ("Trust this device", "信任此设备"), + ("Verification code", "验证码"), + ("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,输入验证码继续登录"), ("Logout", "登出"), ("Tags", "标签"), ("Search ID", "查找ID"), @@ -219,7 +226,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network error", "网络错误"), ("Username missed", "用户名没有填写"), ("Password missed", "密码没有填写"), - ("Wrong credentials", "用户名或者密码错误"), + ("Wrong credentials", "提供的登入信息错误"), ("Edit Tag", "修改标签"), ("Unremember Password", "忘掉密码"), ("Favorites", "收藏"), @@ -271,14 +278,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "是否接受?"), ("Open System Setting", "打开系统设置"), ("How to get Android input permission?", "如何获取安卓的输入权限?"), - ("android_input_permission_tip1", "為了讓遠程設備通過鼠標或者觸屏控制您的安卓設備,你需要允許RustDesk使用\"無障礙\"服務。"), + ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允許RustDesk使用\"无障碍\"服务。"), ("android_input_permission_tip2", "请在接下来的系统设置页面里,找到并进入 [已安装的服务] 页面,将 [RustDesk Input] 服务开启。"), ("android_new_connection_tip", "收到新的连接控制请求,对方想要控制你当前的设备。"), ("android_service_will_start_tip", "开启录屏权限将自动开启服务,允许其他设备向此设备请求建立连接。"), ("android_stop_service_tip", "关闭服务将自动关闭所有已建立的连接。"), ("android_version_audio_tip", "当前安卓版本不支持音频录制,请升级至安卓10或更高。"), ("android_start_service_tip", "点击 [启动服务] 或打开 [屏幕录制] 权限开启手机屏幕共享服务。"), - ("Account", "账号"), + ("Account", "账户"), ("Overwrite", "覆盖"), ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), ("Quit", "退出"), @@ -296,9 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("In privacy mode", "进入隐私模式"), ("Out privacy mode", "退出隐私模式"), ("Language", "语言"), - ("Keep RustDesk background service", "保持RustDesk后台服务"), + ("Keep RustDesk background service", "保持 RustDesk 后台服务"), ("Ignore Battery Optimizations", "忽略电池优化"), - ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的RustDesk应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), + ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的 RustDesk 应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), ("Map mode", "1:1传输"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "适应窗口"), ("General", "常规"), ("Security", "安全"), - ("Account", "账户"), ("Theme", "主题"), ("Dark Theme", "暗黑主题"), ("Dark", "黑暗"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "服务器"), ("Direct IP Access", "IP直接访问"), ("Proxy", "代理"), - ("Port", "端口"), ("Apply", "应用"), ("Disconnect all devices?", "断开所有远程连接?"), ("Clear", "清空"), @@ -374,14 +379,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "其他"), ("Confirm before closing multiple tabs", "关闭多个标签页时向您确认"), ("Keyboard Settings", "键盘设置"), - ("Custom", "自定义"), ("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 桌面或更改您的操作系统。"), ("JumpLink", "查看"), ("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"), - ("Show RustDesk", "显示rustdesk"), + ("Show RustDesk", "显示 RustDesk"), ("This PC", "此电脑"), ("or", "或"), ("Continue with", "使用"), @@ -401,5 +405,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Right click to select tabs", "右键选择选项卡"), ("Skipped", "已跳过"), ("Add to Address Book", "添加到地址簿"), + ("Group", "小组"), + ("Search", "搜索"), + ("Closed manually by the web console", "被web控制台手动关闭"), + ("Local keyboard type", "本地键盘类型"), + ("Select local keyboard type", "请选择本地键盘类型"), + ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装nouveau驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), + ("Always use software rendering", "使用软件渲染"), + ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 33c6492f7..3622aef8a 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Změnit identifikátor"), ("Website", "Webové stránky"), ("About", "O aplikaci"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Ztlumit"), ("Audio Input", "Vstup zvuku"), ("Enhancements", ""), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Vždy se spojovat prostřednictvím brány pro předávání (relay)"), ("whitelist_tip", "Přístup je umožněn pouze z IP adres, nacházejících se na seznamu povolených"), ("Login", "Přihlásit se"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Odhlásit se"), ("Tags", "Štítky"), ("Search ID", "Hledat identifikátor"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Měřítko adaptivní"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04 nebo vyšší verzi."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 1aa53ca57..f07d9914e 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Ændre ID"), ("Website", "Hjemmeside"), ("About", "Omkring"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Sluk for mikrofonen"), ("Audio Input", "Lydindgang"), ("Enhancements", ""), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Forbindelse via relæ-server"), ("whitelist_tip", "Kun IP'er på udgivelseslisten kan få adgang til mig"), ("Login", "Login"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "logger af"), ("Tags", "Nøgleord"), ("Search ID", "Søg ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skaler adaptiv"), ("General", "Generelt"), ("Security", "Sikkerhed"), - ("Account", "Konto"), ("Theme", "Thema"), ("Dark Theme", "Mørk Tema"), ("Dark", "Mørk"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direkte IP Adgang"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Anvend"), ("Disconnect all devices?", "Afbryd alle enheder?"), ("Clear", "Nulstil"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kræver Ubuntu 21.04 eller nyere version."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 223237def..a91f167a2 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -9,21 +9,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Established", "Verbunden"), ("connecting_status", "Verbinden mit dem RustDesk-Netzwerk..."), ("Enable Service", "Vermittlungsdienst aktivieren"), - ("Start Service", "Starte Vermittlungsdienst"), + ("Start Service", "Vermittlungsdienst starten"), ("Service is running", "Vermittlungsdienst aktiv"), ("Service is not running", "Vermittlungsdienst deaktiviert"), - ("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Netzwerkverbindung"), + ("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Netzwerkverbindung."), ("Control Remote Desktop", "Entfernten PC steuern"), ("Transfer File", "Datei übertragen"), ("Connect", "Verbinden"), ("Recent Sessions", "Letzte Sitzungen"), ("Address Book", "Adressbuch"), ("Confirmation", "Bestätigung"), - ("TCP Tunneling", "TCP Tunneln"), + ("TCP Tunneling", "TCP-Tunnelung"), ("Remove", "Entfernen"), ("Refresh random password", "Zufälliges Passwort erzeugen"), ("Set your own password", "Eigenes Passwort setzen"), - ("Enable Keyboard/Mouse", "Tastatur/Maus aktivieren"), + ("Enable Keyboard/Mouse", "Tastatur und Maus aktivieren"), ("Enable Clipboard", "Zwischenablage aktivieren"), ("Enable File Transfer", "Dateiübertragung aktivieren"), ("Enable TCP Tunneling", "TCP-Tunnel aktivieren"), @@ -35,10 +35,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Export server configuration successfully", "Serverkonfiguration erfolgreich exportiert"), ("Invalid server configuration", "Ungültige Serverkonfiguration"), ("Clipboard is empty", "Zwischenablage ist leer"), - ("Stop service", "Vermittlungsdienst deaktivieren"), + ("Stop service", "Vermittlungsdienst stoppen"), ("Change ID", "ID ändern"), ("Website", "Webseite"), ("About", "Über"), + ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt"), + ("Privacy Statement", "Datenschutz"), ("Mute", "Stummschalten"), ("Audio Input", "Audioeingang"), ("Enhancements", "Verbesserungen"), @@ -51,22 +53,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid IP", "Ungültige IP-Adresse"), ("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."), ("Invalid format", "Ungültiges Format"), - ("server_not_support", "Diese Funktion wird noch nicht vom Server unterstützt"), + ("server_not_support", "Diese Funktion wird noch nicht vom Server unterstützt."), ("Not available", "Nicht verfügbar"), ("Too frequent", "Zu häufig"), ("Cancel", "Abbrechen"), ("Skip", "Überspringen"), - ("Close", "Sitzung beenden"), + ("Close", "Schließen"), ("Retry", "Erneut versuchen"), ("OK", "OK"), ("Password Required", "Passwort erforderlich"), - ("Please enter your password", "Bitte geben Sie das Passwort der Gegenstelle ein"), + ("Please enter your password", "Bitte geben Sie Ihr Passwort ein"), ("Remember password", "Passwort merken"), ("Wrong Password", "Falsches Passwort"), ("Do you want to enter again?", "Erneut verbinden?"), ("Connection Error", "Verbindungsfehler"), ("Error", "Fehler"), - ("Reset by the peer", "Verbindung wurde von der Gegenstelle zurückgesetzt"), + ("Reset by the peer", "Verbindung wurde von der Gegenstelle zurückgesetzt."), ("Connecting...", "Verbindung wird hergestellt..."), ("Connection in progress. Please wait.", "Die Verbindung wird hergestellt. Bitte warten..."), ("Please try 1 minute later", "Bitte versuchen Sie es später erneut"), @@ -92,7 +94,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select All", "Alles auswählen"), ("Unselect All", "Alles abwählen"), ("Empty Directory", "Leerer Ordner"), - ("Not an empty directory", "Ordner ist nicht leer"), + ("Not an empty directory", "Ordner ist nicht leer."), ("Are you sure you want to delete this file?", "Sind Sie sicher, dass Sie diese Datei löschen wollen?"), ("Are you sure you want to delete this empty directory?", "Sind Sie sicher, dass Sie diesen leeren Ordner löschen möchten?"), ("Are you sure you want to delete the file of this directory?", "Sind Sie sicher, dass Sie die Datei dieses Ordners löschen möchten?"), @@ -111,7 +113,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Verkleinern"), ("Stretch", "Strecken"), - ("Scrollbar", "Scrollleiste"), + ("Scrollbar", "Scroll-Leiste"), ("ScrollAuto", "Automatisch scrollen"), ("Good image quality", "Hohe Bildqualität"), ("Balanced", "Ausgeglichen"), @@ -124,11 +126,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insert", "Einfügen"), ("Insert Lock", "Win+L (Sperren) senden"), ("Refresh", "Aktualisieren"), - ("ID does not exist", "Diese ID existiert nicht"), + ("ID does not exist", "Diese ID existiert nicht."), ("Failed to connect to rendezvous server", "Verbindung zum Vermittlungsserver fehlgeschlagen"), - ("Please try later", "Bitte versuchen Sie es später erneut"), - ("Remote desktop is offline", "Entfernter PC ist offline"), - ("Key mismatch", "Schlüssel stimmt nicht überein"), + ("Please try later", "Bitte versuchen Sie es später erneut."), + ("Remote desktop is offline", "Entfernter PC ist offline."), + ("Key mismatch", "Schlüssel stimmen nicht überein."), ("Timeout", "Zeitüberschreitung"), ("Failed to connect to relay server", "Verbindung zum Vermittlungsserver fehlgeschlagen"), ("Failed to connect via rendezvous server", "Verbindung über Vermittlungsserver ist fehlgeschlagen"), @@ -136,34 +138,34 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten PC fehlgeschlagen"), ("Set Password", "Passwort festlegen"), ("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, um RustDesk auf dem System zu installieren"), + ("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", "Upgrade"), ("Click to download", "Zum Herunterladen klicken"), ("Click to update", "Update"), ("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 \"Bildschirm-Aufnahme\"-Berechtigung erteilen."), + ("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk die Berechtigung \"Bildschirmaufnahme\" erteilen."), ("Installing ...", "Installiere..."), ("Install", "Installieren"), ("Installation", "Installation"), ("Installation Path", "Installationspfad"), ("Create start menu shortcuts", "Verknüpfung im Startmenü erstellen"), ("Create desktop icon", "Desktop-Verknüpfung erstellen"), - ("agreement_tip", "Durch die Installation akzeptieren Sie die Lizenzvereinbarung"), + ("agreement_tip", "Durch die Installation akzeptieren Sie die Lizenzvereinbarung."), ("Accept and Install", "Akzeptieren und Installieren"), ("End-user license agreement", "Lizenzvereinbarung für Endbenutzer"), ("Generating ...", "Wird generiert..."), ("Your installation is lower version.", "Ihre Version ist veraltet."), ("not_close_tcp_tip", "Schließen Sie dieses Fenster nicht, solange Sie den Tunnel benutzen."), - ("Listening ...", "Lausche..."), + ("Listening ...", "Lauschen..."), ("Remote Host", "Entfernter PC"), ("Remote Port", "Entfernter Port"), ("Action", "Aktion"), ("Add", "Hinzufügen"), ("Local Port", "Lokaler Port"), - ("Local Address", "Lokale Addresse"), + ("Local Address", "Lokale Adresse"), ("Change Local Port", "Lokalen Port ändern"), - ("setup_server_tip", "für eine schnellere Verbindung richten Sie bitte Ihren eigenen Verbindungsserver ein."), + ("setup_server_tip", "für eine schnellere Verbindung richten Sie bitte Ihren eigenen Server ein."), ("Too short, at least 6 characters.", "Zu kurz, mindestens 6 Zeichen."), ("The confirmation is not identical.", "Die Passwörter stimmen nicht überein."), ("Permissions", "Berechtigungen"), @@ -183,7 +185,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter your password", "Geben Sie Ihr Passwort ein"), ("Logging in...", "Anmelden..."), ("Enable RDP session sharing", "RDP-Sitzungsfreigabe aktivieren"), - ("Auto Login", "Automatisch anmelden (nur gültig, wenn Sie \"Sperren nach Sitzungsende\" aktiviert haben)"), + ("Auto Login", "Automatisch anmelden (nur gültig, wenn Sie \"Nach Sitzungsende sperren\" aktiviert haben)"), ("Enable Direct IP Access", "Direkten IP-Zugang aktivieren"), ("Rename", "Umbenennen"), ("Space", "Speicherplatz"), @@ -193,7 +195,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "Bitte geben Sie den Ordnernamen ein"), ("Fix it", "Reparieren"), ("Warning", "Warnung"), - ("Login screen using Wayland is not supported", "Anmeldebildschirm wird mit Wayland nicht unterstützt"), + ("Login screen using Wayland is not supported", "Anmeldebildschirm mit Wayland wird nicht unterstützt."), ("Reboot required", "Neustart erforderlich"), ("Unsupported display server ", "Nicht unterstützter Display-Server"), ("x11 expected", "X11 erwartet"), @@ -206,12 +208,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Run without install", "Ohne Installation ausführen"), ("Always connected via relay", "Immer über Relay-Server verbunden"), ("Always connect via relay", "Immer über Relay-Server verbinden"), - ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen"), + ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Abmelden"), ("Tags", "Schlagworte"), ("Search ID", "Suche ID"), - ("Current Wayland display server is not supported", "Der aktuelle Wayland-Anzeigeserver wird nicht unterstützt"), + ("Current Wayland display server is not supported", "Der aktuelle Wayland-Anzeigeserver wird nicht unterstützt."), ("whitelist_sep", "Getrennt durch Komma, Semikolon, Leerzeichen oder Zeilenumbruch"), ("Add ID", "ID hinzufügen"), ("Add Tag", "Stichwort hinzufügen"), @@ -227,17 +234,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remove from Favorites", "Aus Favoriten entfernen"), ("Empty", "Keine Einträge"), ("Invalid folder name", "Ungültiger Ordnername"), - ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5 Proxy", "SOCKS5-Proxy"), ("Hostname", "Hostname"), ("Discovered", "Im LAN erkannt"), - ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein"), + ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein."), ("Remote ID", "Entfernte ID"), ("Paste", "Einfügen"), ("Paste here?", "Hier einfügen?"), ("Are you sure to close the connection?", "Möchten Sie diese Verbindung wirklich trennen?"), ("Download new version", "Neue Version herunterladen"), ("Touch mode", "Touch-Modus"), - ("Mouse mode", "Maus-Modus"), + ("Mouse mode", "Mausmodus"), ("One-Finger Tap", "1-Finger-Tipp"), ("Left Mouse", "Linksklick"), ("One-Long Tap", "1-Finger-Halten"), @@ -253,8 +260,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Pinch to Zoom", "2-Finger-Zoom"), ("Canvas Zoom", "Sichtfeld-Zoom"), ("Reset canvas", "Sichtfeld zurücksetzen"), - ("No permission of file transfer", "Keine Berechtigung für den Dateizugriff"), - ("Note", "Anmerkung"), + ("No permission of file transfer", "Keine Berechtigung für die Dateiübertragung"), + ("Note", "Hinweis"), ("Connection", "Verbindung"), ("Share Screen", "Bildschirm freigeben"), ("CLOSE", "DEAKTIV."), @@ -271,8 +278,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "Verbindung zulassen?"), ("Open System Setting", "Systemeinstellung öffnen"), ("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"), - ("android_input_permission_tip1", "Damit ein Remote-Gerät Ihr Android-Gerät steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."), - ("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen und geben Sie [Installierte Dienste] ein, schalten Sie den Dienst [RustDesk Input] ein."), + ("android_input_permission_tip1", "Damit ein entferntes Gerät Ihr Android-Gerät steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."), + ("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen Sie [Installierte Dienste] und schalten Sie den Dienst [RustDesk Input] ein."), ("android_new_connection_tip", "möchte ihr Gerät steuern."), ("android_service_will_start_tip", "Durch das Aktivieren der Bildschirmfreigabe wird der Dienst automatisch gestartet, sodass andere Geräte dieses Android-Gerät steuern können."), ("android_stop_service_tip", "Durch das Deaktivieren des Dienstes werden automatisch alle hergestellten Verbindungen getrennt."), @@ -288,20 +295,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Succeeded", "Erfolgreich"), ("Someone turns on privacy mode, exit", "Jemand hat den Datenschutzmodus aktiviert, beende..."), ("Unsupported", "Nicht unterstützt"), - ("Peer denied", "Die Gegenstelle hat die Verbindung abgelehnt"), + ("Peer denied", "Die Gegenstelle hat die Verbindung abgelehnt."), ("Please install plugins", "Bitte installieren Sie Plugins"), - ("Peer exit", "Die Gegenstelle hat die Verbindung getrennt"), + ("Peer exit", "Die Gegenstelle hat die Verbindung getrennt."), ("Failed to turn off", "Ausschalten fehlgeschlagen"), ("Turned off", "Ausgeschaltet"), ("In privacy mode", "Datenschutzmodus aktivieren"), ("Out privacy mode", "Datenschutzmodus deaktivieren"), ("Language", "Sprache"), ("Keep RustDesk background service", "RustDesk im Hintergrund ausführen"), - ("Ignore Battery Optimizations", "Batterieoptimierung ignorieren"), - ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Batterieopimierung öffnen?"), + ("Ignore Battery Optimizations", "Akkuoptimierung ignorieren"), + ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Akkuoptimierung öffnen?"), ("Connection not allowed", "Verbindung abgelehnt"), ("Legacy mode", "Kompatibilitätsmodus"), - ("Map mode", ""), //Muss noch angepasst wer"), + ("Map mode", "Kartenmodus"), ("Translate mode", "Übersetzungsmodus"), ("Use permanent password", "Permanentes Passwort verwenden"), ("Use both passwords", "Beide Passwörter verwenden"), @@ -328,16 +335,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Relaisverbindung"), ("Secure Connection", "Sichere Verbindung"), ("Insecure Connection", "Unsichere Verbindung"), - ("Scale original", "Keine Saklierung"), - ("Scale adaptive", "Automatische Saklierung"), + ("Scale original", "Keine Skalierung"), + ("Scale adaptive", "Automatische Skalierung"), ("General", "Allgemein"), ("Security", "Sicherheit"), - ("Account", "Konto"), ("Theme", "Farbgebung"), - ("Dark Theme", "dunkle Farbgebung"), + ("Dark Theme", "Dunkle Farbgebung"), ("Dark", "Dunkel"), ("Light", "Hell"), - ("Follow System", "System-Standard"), + ("Follow System", "Systemstandard"), ("Enable hardware codec", "Hardware-Codec aktivieren"), ("Unlock Security Settings", "Sicherheitseinstellungen entsperren"), ("Enable Audio", "Audio aktivieren"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direkter IP-Zugriff"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Anwenden"), ("Disconnect all devices?", "Alle Geräte trennen?"), ("Clear", "Zurücksetzen"), @@ -358,7 +363,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin menubar", "Menüleiste lösen"), ("Recording", "Aufnahme"), ("Directory", "Verzeichnis"), - ("Automatically record incoming sessions", "Automatische Aufzeichnung eingehender Sitzungen"), + ("Automatically record incoming sessions", "Eingehende Sitzungen automatisch aufzeichnen"), ("Change", "Ändern"), ("Start session recording", "Sitzungsaufzeichnung starten"), ("Stop session recording", "Sitzungsaufzeichnung beenden"), @@ -367,14 +372,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "LAN-Erkennung aktivieren"), ("Deny LAN Discovery", "LAN-Erkennung verbieten"), ("Write a message", "Nachricht schreiben"), - ("Prompt", ""), //Aufforderu"), + ("Prompt", "Meldung"), ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten..."), - ("elevated_foreground_window_tip", ""), + ("elevated_foreground_window_tip", "Das aktuell geöffnete Fenster des ferngesteuerten Computers benötigt höhere Rechte. Deshalb ist es derzeit nicht möglich, die Maus und die Tastatur zu verwenden. Bitten Sie den Nutzer, dessen Computer Sie fernsteuern, das Fenster zu minimieren oder die Rechte zu erhöhen. Um dieses Problem zukünftig zu vermeiden, wird empfohlen, die Software auf dem ferngesteuerten Computer zu installieren."), ("Disconnected", "Verbindung abgebrochen"), ("Other", "Weitere Einstellungen"), ("Confirm before closing multiple tabs", "Nachfragen, wenn mehrere Tabs geschlossen werden"), ("Keyboard Settings", "Tastatureinstellungen"), - ("Custom", "Individuell"), ("Full Access", "Vollzugriff"), ("Screen Share", "Bildschirmfreigabe"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."), @@ -389,16 +393,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Zoom cursor", "Cursor zoomen"), ("Accept sessions via password", "Sitzung mit Passwort bestätigen"), ("Accept sessions via click", "Sitzung mit einem Klick bestätigen"), - ("Accept sessions via both", "Sitzung durch Klick und Passwort bestätigen"), - ("Please wait for the remote side to accept your session request...", "Bitte warten Sie auf die Gegenstelle, dass diese Ihre Sitzungsanfrage bestätigt..."), + ("Accept sessions via both", "Sitzung mit Klick und Passwort bestätigen"), + ("Please wait for the remote side to accept your session request...", "Bitte warten Sie, bis die Gegenseite Ihre Sitzungsanfrage akzeptiert hat..."), ("One-time Password", "Einmalpasswort"), ("Use one-time password", "Einmalpasswort verwenden"), ("One-time password length", "Länge des Einmalpassworts"), ("Request access to your device", "Zugriff zu Ihrem Gerät erbitten"), ("Hide connection management window", "Fenster zur Verwaltung der Verbindung verstecken"), - ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff nur über ein permanentes Passwort erfolgt."), // Sehr unklar. Muss noch angepasst werden. Original: Allow hiding only if accepting sessions via password and using pernament passw"), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), - ("Add to Address Book", ""), + ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff über ein permanentes Passwort erfolgt."), + ("wayland_experiment_tip", "Die Unterstützung von Wayland ist nur experimentell. Bitte nutzen Sie X11, wenn Sie einen unbeaufsichtigten Zugriff benötigen."), + ("Right click to select tabs", "Register mit rechtem Mausklick auswählen"), + ("Skipped", "Übersprungen"), + ("Add to Address Book", "Zum Adressbuch hinzufügen"), + ("Group", "Gruppe"), + ("Search", "Suchen"), + ("Closed manually by the web console", "Manuell über die Webkonsole beendet"), + ("Local keyboard type", "Lokaler Tastaturtyp"), + ("Select local keyboard type", "Lokalen Tastaturtyp auswählen"), + ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), + ("Always use software rendering", "Software-Rendering immer verwenden"), + ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk \"Input Monitoring\"-Rechte erteilen."), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index b6992230d..b718fc0f9 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -35,5 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop service", "Stop Service"), ("hide_cm_tip", "Allow hiding only if accepting sessions via password and using permanent password"), ("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."), + ("Slogan_tip", "Made with heart in this chaotic world!"), + ("verification_tip", "A new device has been detected, and a verification code has been sent to the registered email address, enter the verification code to continue logging in."), + ("software_render_tip", "If you have an Nvidia graphics card and the remote window closes immediately after connecting, installing the nouveau driver and choosing to use software rendering may help. A software restart is required."), + ("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index c2748a9bc..2a41fdcf9 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Ŝanĝi identigilon"), ("Website", "Retejo"), ("About", "Pri"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Muta"), ("Audio Input", "Aŭdia enigo"), ("Enhancements", ""), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Ĉiam konekti per relajso"), ("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"), ("Login", "Konekti"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Malkonekti"), ("Tags", "Etikedi"), ("Search ID", "Serĉi ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skalo adapta"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland postulas Ubuntu 21.04 aŭ pli altan version."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 17c3ddf07..e0e410711 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -17,7 +17,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Transfer File", "Transferir archivo"), ("Connect", "Conectar"), ("Recent Sessions", "Sesiones recientes"), - ("Address Book", "Directorio"), + ("Address Book", "Libreta de direcciones"), ("Confirmation", "Confirmación"), ("TCP Tunneling", "Túnel TCP"), ("Remove", "Quitar"), @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Cambiar ID"), ("Website", "Sitio web"), ("About", "Acerca de"), + ("Slogan_tip", "Hecho con corazón en este mundo caótico!"), + ("Privacy Statement", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada de audio"), ("Enhancements", "Mejoras"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Conéctese siempre a través de relay"), ("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"), ("Login", "Iniciar sesión"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Salir"), ("Tags", "Tags"), ("Search ID", "Buscar ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptativa"), ("General", ""), ("Security", "Seguridad"), - ("Account", "Cuenta"), ("Theme", "Tema"), ("Dark Theme", "Tema Oscuro"), ("Dark", "Oscuro"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Servidor"), ("Direct IP Access", "Acceso IP Directo"), ("Proxy", ""), - ("Port", "Puerto"), ("Apply", "Aplicar"), ("Disconnect all devices?", "¿Desconectar todos los dispositivos?"), ("Clear", "Borrar"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Otro"), ("Confirm before closing multiple tabs", "Confirmar antes de cerrar múltiples pestañas"), ("Keyboard Settings", "Ajustes de teclado"), - ("Custom", "Personalizado"), ("Full Access", "Acceso completo"), ("Screen Share", "Compartir pantalla"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requiere Ubuntu 21.04 o una versión superior."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Permitir ocultar solo si se aceptan sesiones a través de contraseña y usando contraseña permanente"), ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), + ("Skipped", ""), ("Add to Address Book", "Añadir a la libreta de direcciones"), + ("Group", "Grupo"), + ("Search", "Búsqueda"), + ("Closed manually by the web console", "Cerrado manualmente por la consola web"), + ("Local keyboard type", "Tipo de teclado local"), + ("Select local keyboard type", "Seleccionar tipo de teclado local"), + ("software_render_tip", "Si tienes una gráfica Nvidia y la ventana remota se cierra inmediatamente, instalar el driver nouveau y elegir renderizado por software podría ayudar. Se requiere reiniciar la aplicación."), + ("Always use software rendering", "Usar siempre renderizado por software"), + ("config_input", "Para controlar el escritorio remoto con el teclado necesitas dar a RustDesk permisos de \"Monitorización de entrada\"."), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 1e1689cbb..790d01682 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -9,22 +9,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Established", "اتصال برقرار شد"), ("connecting_status", "...در حال برقراری ارتباط با سرور"), ("Enable Service", "فعالسازی سرویس"), - ("Start Service", "اجرا سرویس"), + ("Start Service", "اجرای سرویس"), ("Service is running", "سرویس در حال اجرا است"), ("Service is not running", "سرویس اجرا نشده"), ("not_ready_status", "ارتباط برقرار نشد. لطفا شبکه خود را بررسی کنید"), ("Control Remote Desktop", "کنترل دسکتاپ میزبان"), - ("Transfer File", "جابه جایی فایل"), + ("Transfer File", "انتقال فایل"), ("Connect", "اتصال"), ("Recent Sessions", "جلسات اخیر"), ("Address Book", "دفترچه آدرس"), ("Confirmation", "تایید"), ("TCP Tunneling", "TCP تانل"), ("Remove", "حذف"), - ("Refresh random password", "رمز عبور تصادفی را بروز کنید"), + ("Refresh random password", "بروزرسانی رمز عبور تصادفی"), ("Set your own password", "!رمز عبور دلخواه بگذارید"), - ("Enable Keyboard/Mouse", "Keyboard/Mouse فعالسازی"), - ("Enable Clipboard", "Clipboard فعالسازی"), + ("Enable Keyboard/Mouse", " فعالسازی ماوس/صفحه کلید"), + ("Enable Clipboard", "فعال سازی کلیپبورد"), ("Enable File Transfer", "انتقال فایل را فعال کنید"), ("Enable TCP Tunneling", "را فعال کنید TCP تانل"), ("IP Whitelisting", "های مجاز IP لیست"), @@ -34,11 +34,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Import server configuration successfully", "تنظیمات سرور با فایل کانفیگ با موفقیت انجام شد"), ("Export server configuration successfully", "ایجاد فایل کانفیگ از تنظیمات فعلی با موفقیت انجام شد"), ("Invalid server configuration", "تنظیمات سرور نامعتبر است"), - ("Clipboard is empty", "خالی است Clipboard"), + ("Clipboard is empty", "کلیپبورد خالی است"), ("Stop service", "توقف سرویس"), ("Change ID", "تعویض شناسه"), ("Website", "وب سایت"), ("About", "درباره"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "بستن صدا"), ("Audio Input", "ورودی صدا"), ("Enhancements", "بهبودها"), @@ -50,10 +52,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("invalid_http", "شروع شود http:// یا https:// باید با"), ("Invalid IP", "نامعتبر است IP آدرس"), ("id_change_tip", "شناسه باید طبق این شرایط باشد : حروف کوچک و بزرگ انگلیسی و اعداد از 0 تا 9، _ و همچنین حرف اول آن فقط حروف بزرگ یا کوچک انگلیسی و طول آن بین 6 الی 16 کاراکتر باشد"), - ("Invalid format", "فرمت نادرس است"), + ("Invalid format", "فرمت نادرست است"), ("server_not_support", "هنوز توسط سرور مورد نظر پشتیبانی نمی شود"), ("Not available", "در دسترسی نیست"), - ("Too frequent", "تعداد زیاد"), + ("Too frequent", "خیلی رایج"), ("Cancel", "لغو"), ("Skip", "رد کردن"), ("Close", "بستن"), @@ -72,7 +74,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please try 1 minute later", "لطفا بعد از 1 دقیقه مجددا تلاش کنید"), ("Login Error", "ورود ناموفق بود"), ("Successful", "ورود با موفقیت انجام شد"), - ("Connected, waiting for image...", "ارتباط وصل شد. برای دریافت تصویر دسکتاپ میزبان منتظر بمانید..."), + ("Connected, waiting for image...", "...ارتباط برقرار شد. انتظار برای دریافت تصاویر"), ("Name", "نام"), ("Type", "نوع فایل"), ("Modified", "تاریخ تغییر"), @@ -84,20 +86,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local", "محلی"), ("Remote", "از راه دور"), ("Remote Computer", "سیستم میزبان"), - ("Local Computer", "سیستم از راه دور"), - ("Confirm Delete", "حذف را تایید کنید"), + ("Local Computer", "سیستم راه دور"), + ("Confirm Delete", "تایید حذف"), ("Delete", "حذف"), - ("Properties", "Properties"), - ("Multi Select", "انتخاب همزمان"), + ("Properties", "مشخصات"), + ("Multi Select", "انتخاب دسته ای"), ("Select All", "انتخاب همه"), - ("Unselect 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!", "این برگشت ناپذیر است!"), + ("Do this for all conflicts", "این عمل برای همه ی تضادها انجام شود"), + ("This is irreversible!", "این اقدام برگشت ناپذیر است!"), ("Deleting", "در حال حذف"), ("files", "فایل ها"), ("Waiting", "انتظار"), @@ -105,21 +107,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Speed", "سرعت"), ("Custom Image Quality", "سفارشی سازی کیفیت تصاویر"), ("Privacy mode", "حالت حریم خصوصی"), - ("Block user input", "ورودی کاربر را مسدود کنید"), - ("Unblock user input", "قفل ورودی کاربر را باز کنید"), - ("Adjust Window", "پنجره را تنظیم کنید"), + ("Block user input", "بلاک کردن ورودی کاربر"), + ("Unblock user input", "آنبلاک کردن ورودی کاربر"), + ("Adjust Window", "تنظیم پنجره"), ("Original", "اصل"), - ("Shrink", ""), - ("Stretch", ""), - ("Scrollbar", ""), - ("ScrollAuto", ""), + ("Shrink", "کوچک کردن"), + ("Stretch", "کشیدن تصویر"), + ("Scrollbar", "اسکرول بار"), + ("ScrollAuto", "پیمایش/اسکرول خودکار"), ("Good image quality", "کیفیت خوب تصویر"), ("Balanced", "متعادل"), - ("Optimize reaction time", "زمان واکنش را بهینه کنید"), + ("Optimize reaction time", "بهینه سازی زمان واکنش"), ("Custom", "سفارشی"), ("Show remote cursor", "نمایش مکان نما موس میزبان"), ("Show quality monitor", "نمایش کیفیت مانیتور"), - ("Disable clipboard", "Clipboard غیرفعالسازی"), + ("Disable clipboard", " غیرفعالسازی کلیپبورد"), ("Lock after session end", "قفل کردن حساب کاربری سیستم عامل پس از پایان جلسه"), ("Insert", "افزودن"), ("Insert Lock", "افزودن قفل"), @@ -127,23 +129,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID does not exist", "شناسه وجود ندارد"), ("Failed to connect to rendezvous server", "اتصال به سرور تولید شناسه انجام نشد"), ("Please try later", "لطفا بعدا تلاش کنید"), - ("Remote desktop is offline", "دسکتاپ از راه دور خاموش است"), + ("Remote desktop is offline", "دسکتاپ راه دور آفلاین است"), ("Key mismatch", "عدم تطابق کلید"), ("Timeout", "زمان انتظار به پایان رسید"), ("Failed to connect to relay server", "سرور وصل نشد Relay به"), ("Failed to connect via rendezvous server", "اتصال از طریق سرور تولید شناسه انجام نشد"), ("Failed to connect via relay server", "انجام نشد Relay اتصال از طریق سرور"), - ("Failed to make direct connection to remote desktop", "اتصال مستقیم به دسکتاپ از راه دور با موفقیت انجام نشد"), - ("Set Password", "اختصاص رمزعبور"), + ("Failed to make direct connection to remote desktop", "اتصال مستقیم به دسکتاپ راه دور انجام نشد"), + ("Set Password", "تنظیم رمزعبور"), ("OS Password", "رمز عیور سیستم عامل"), ("install_tip", "لطفا برنامه را نصب کنید UAC و جلوگیری از خطای RustDesk برای راحتی در استفاده از نرم افزار"), ("Click to upgrade", "برای ارتقا کلیک کنید"), ("Click to download", "برای دانلود کلیک کنید"), ("Click to update", "برای به روز رسانی کلیک کنید"), ("Configure", "تنظیم"), - ("config_acc", "برای کنترل از راه دور دسکتاپ، باید به RustDesk مجوز \"access\" بدهید"), - ("config_screen", "برای دسترسی از راه دور به دسکتاپ خود، باید به RustDesk مجوزهای \"screenshot\" بدهید."), - ("Installing ...", "در حال نصب..."), + ("config_acc", "بدهید \"access\" مجوز RustDesk برای کنترل از راه دور دسکتاپ باید به"), + ("config_screen", "بدهید \"screenshot\" مجوز RustDesk برای کنترل از راه دور دسکتاپ باید به"), + ("Installing ...", "...در حال نصب"), ("Install", "نصب"), ("Installation", "نصب و راه اندازی"), ("Installation Path", "محل نصب"), @@ -152,28 +154,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "با شروع نصب، شرایط توافق نامه مجوز را می پذیرید"), ("Accept and Install", "قبول و شروع نصب"), ("End-user license agreement", "قرارداد مجوز کاربر نهایی"), - ("Generating ...", "پدید آوردن..."), - ("Your installation is lower version.", "نسخه قبلی نصب شده است"), + ("Generating ...", "...در حال تولید"), + ("Your installation is lower version.", "نسخه قدیمی تری نصب شده است"), ("not_close_tcp_tip", "هنگام استفاده از تونل این پنجره را نبندید"), - ("Listening ...", "انتظار..."), - ("Remote Host", "دستگاه از راه دور"), + ("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", "قطع اتصال"), - ("Allow using keyboard and mouse", "اجازه استفاده از صفحه کلید و ماوس را بدهید"), - ("Allow using clipboard", "را بدهید Clipboard اجازه استفاده از"), - ("Allow hearing sound", "اجازه شنیدن صدا را بدهید"), - ("Allow file copy and paste", "اجازه کپی و چسباندن فایل را بدهید"), + ("Allow using keyboard and mouse", "مجاز بودن استفاده از صفحه کلید و ماوس"), + ("Allow using clipboard", "مجاز بودن استفاده از کلیپبورد"), + ("Allow hearing sound", "مجاز بودن شنیدن صدا"), + ("Allow file copy and paste", "مجاز بودن کپی و چسباندن فایل"), ("Connected", "متصل شده"), ("Direct and encrypted connection", "اتصال مستقیم و رمزگذاری شده"), ("Relayed and encrypted connection", "و رمزگذاری شده Relay اتصال از طریق"), @@ -181,7 +183,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and unencrypted connection", "و رمزگذاری نشده Relay اتصال از طریق"), ("Enter Remote ID", "شناسه از راه دور را وارد کنید"), ("Enter your password", "زمر عبور خود را وارد کنید"), - ("Logging in...", "در حال ورود..."), + ("Logging in...", "...در حال ورود"), ("Enable RDP session sharing", "اشتراک گذاری جلسه RDP را فعال کنید"), ("Auto Login", "ورود خودکار"), ("Enable Direct IP Access", "دسترسی مستقیم IP را فعال کنید"), @@ -193,7 +195,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", ""), @@ -202,16 +204,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username", "نام کاربری"), ("Invalid port", "پورت نامعتبر است"), ("Closed manually by the peer", "به صورت دستی توسط میزبان بسته شد"), - ("Enable remote configuration modification", "تغییرات پیکربندی از راه دور را مجاز کنید"), + ("Enable remote configuration modification", "فعال بودن اعمال تغییرات پیکربندی از راه دور"), ("Run without install", "بدون نصب اجرا شود"), ("Always connected via relay", "متصل است Relay همیشه با"), - ("Always connect via relay", "برای اتصال استفاده کنید Relay از"), - ("whitelist_tip", "فقط آدرس های IP مجاز می توانند به این دسکتاپ متصل شوند"), + ("Always connect via relay", "برای اتصال استفاده شود Relay از"), + ("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"), ("Login", "ورود"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "خروج"), ("Tags", "برچسب ها"), ("Search ID", "جستجوی شناسه"), - ("Current Wayland display server is not supported", "سرور نمای فعلی Wayland پشتیبانی نمی شود"), + ("Current Wayland display server is not supported", "پشتیبانی نمی شود Wayland سرور نمایش فعلی"), ("whitelist_sep", "با کاما، نقطه ویرگول، فاصله یا خط جدید از هم جدا می شوند"), ("Add ID", "افزودن شناسه"), ("Add Tag", "افزودن برچسب"), @@ -220,19 +227,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "نام کاربری وجود ندارد"), ("Password missed", "رمزعبور وجود ندارد"), ("Wrong credentials", "اعتبارنامه نادرست است"), - ("Edit Tag", "برچسب را تغییر دهید"), - ("Unremember Password", "رمز عبور را ذخیره نکنید"), - ("Favorites", "موارد دلخواه"), + ("Edit Tag", "ویرایش برچسب"), + ("Unremember Password", "رمز عبور ذخیره نشود"), + ("Favorites", "اتصالات دلخواه"), ("Add to Favorites", "افزودن به علاقه مندی ها"), ("Remove from Favorites", "از علاقه مندی ها حذف شود"), ("Empty", "موردی وجود ندارد"), ("Invalid folder name", "نام پوشه نامعتبر است"), ("Socks5 Proxy", "Socks5 Proxy"), - ("Hostname", "Hostname"), + ("Hostname", "نام هاست"), ("Discovered", "پیدا شده"), ("install_daemon_tip", "برای شروع در هنگام راه اندازی، باید سرویس سیستم را نصب کنید"), - ("Remote ID", "شناسه از راه دور"), - ("Paste", "درج کنید"), + ("Remote ID", "شناسه راه دور"), + ("Paste", "درج"), ("Paste here?", "اینجا درج شود؟"), ("Are you sure to close the connection?", "آیا مطمئن هستید که می خواهید اتصال را پایان دهید؟"), ("Download new version", "دانلود نسخه جدید"), @@ -241,7 +248,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-Finger Tap", "با یک انگشت لمس کنید"), ("Left Mouse", "دکمه سمت چپ ماوس"), ("One-Long Tap", "لمس طولانی با یک انگشت"), - ("Two-Finger Tap", "با دو انگشت لمس کنید"), + ("Two-Finger Tap", "لمس دو انگشتی"), ("Right Mouse", "دکمه سمت راست ماوس"), ("One-Finger Move", "با یک انگشت حرکت کنید"), ("Double Tap & Move", "دو ضربه سریع بزنید و حرکت دهید"), @@ -250,7 +257,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Mouse Wheel", "چرخ ماوس"), ("Two-Finger Move", "با دو انگشت حرکت کنید"), ("Canvas Move", ""), - ("Pinch to Zoom", "زوم را کوچک کنید"), + ("Pinch to Zoom", "با دو انگشت بکشید تا زوم شود"), ("Canvas Zoom", ""), ("Reset canvas", ""), ("No permission of file transfer", "مجوز انتقال فایل داده نشده"), @@ -261,14 +268,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OPEN", "باز کردن"), ("Chat", "چت"), ("Total", "مجموع"), - ("items", "موارد"), + ("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", "برای اینکه یک دستگاه راه دور بتواند دستگاه Android شما را از طریق ماوس یا لمسی کنترل کند، باید به RustDesk اجازه دهید از ویژگی \"Accessibility\" استفاده کند."), @@ -280,7 +287,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_start_service_tip", "برای شروع سرویس اشتراک‌گذاری صفحه، روی مجوز \"شروع مرحله‌بندی سرور\" یا OPEN \"Screen Capture\" کلیک کنید."), ("Account", "حساب کاربری"), ("Overwrite", "بازنویسی"), - ("This file exists, skip or overwrite this file?", "این فایل وجود دارد، از فایل رد شود یا بازنویسی شود؟"), + ("This file exists, skip or overwrite this file?", "این فایل وجود دارد، از فایل رد شود یا آن را بازنویسی کند؟"), ("Quit", "خروج"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "راهنما"), @@ -291,27 +298,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Peer denied", "توسط میزبان راه دور رد شد"), ("Please install plugins", "لطفا افزونه ها را نصب کنید"), ("Peer exit", "میزبان خارج شد"), - ("Failed to turn off", "خاموش کردن با موفقیت انجام نشد"), + ("Failed to turn off", "خاموش کردن انجام نشد"), ("Turned off", "خاموش شد"), ("In privacy mode", "در حالت حریم خصوصی"), ("Out privacy mode", "خارج از حالت حریم خصوصی"), ("Language", "زبان"), - ("Keep RustDesk background service", "سرویس RustDesk را در پس زمینه نگه دارید"), - ("Ignore Battery Optimizations", "بهینه سازی باتری را نادیده بگیرید"), + ("Keep RustDesk background service", "را در پس زمینه نگه دارید RustDesk سرویس"), + ("Ignore Battery Optimizations", "بهینه سازی باتری نادیده گرفته شود"), ("android_open_battery_optimizations_tip", "به صفحه تنظیمات بعدی بروید"), ("Connection not allowed", "اتصال مجاز نیست"), - ("Legacy mode", "پشتیبانی موارد قدیمی"), - ("Map mode", "حالت نقشه"), + ("Legacy mode", "legacy حالت"), + ("Map mode", "map حالت"), ("Translate mode", "حالت ترجمه"), - ("Use permanent password", "از رمز عبور دائمی استفاده کنید"), - ("Use both passwords", "از هر دو رمز عبور استفاده کنید"), - ("Set permanent password", "یک رمز عبور دائمی تنظیم کنید"), - ("Enable Remote Restart", "فعال کردن راه‌اندازی مجدد از راه دور"), - ("Allow remote restart", "اجازه راه اندازی مجدد از راه دور"), - ("Restart Remote Device", "راه‌اندازی مجدد دستگاه از راه دور"), + ("Use permanent password", "از رمز عبور دائمی استفاده شود"), + ("Use both passwords", "از هر دو رمز عبور استفاده شود"), + ("Set permanent password", "یک رمز عبور دائمی تنظیم شود"), + ("Enable Remote Restart", "فعال کردن قابلیت ریستارت از راه دور"), + ("Allow remote restart", "مجاز بودن ریستارت از راه دور"), + ("Restart Remote Device", "ریستارت کردن از راه دور"), ("Are you sure you want to restart", "ایا مطمئن هستید میخواهید راه اندازی مجدد انجام بدید؟"), - ("Restarting Remote Device", "راه اندازی مجدد یک دستگاه راه دور"), - ("remote_restarting_tip", "دستگاه راه دور دوباره راه اندازی می شود. این پیام را ببندید و پس از مدتی با استفاده از یک رمز عبور دائمی دوباره وصل شوید."), + ("Restarting Remote Device", "در حال راه اندازی مجدد دستگاه راه دور"), + ("remote_restarting_tip", "دستگاه راه دور در حال راه اندازی مجدد است. این پیام را ببندید و پس از مدتی با استفاده از یک رمز عبور دائمی دوباره وصل شوید."), ("Copied", "کپی شده است"), ("Exit Fullscreen", "از حالت تمام صفحه خارج شوید"), ("Fullscreen", "تمام صفحه"), @@ -332,30 +339,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "مقیاس تطبیقی"), ("General", "عمومی"), ("Security", "امنیت"), - ("Account", "حساب کاربری"), ("Theme", "نمایه"), ("Dark Theme", "نمایه تیره"), ("Dark", "تیره"), ("Light", "روشن"), - ("Follow System", "سیستم را دنبال کنید"), - ("Enable hardware codec", "از کدک سخت افزاری استفاده کنید"), - ("Unlock Security Settings", "تنظیمات امنیتی را باز کنید"), - ("Enable Audio", "صدا را روشن کنید"), - ("Unlock Network Settings", "باز کردن قفل تنظیمات شبکه"), + ("Follow System", "پیروی از سیستم"), + ("Enable hardware codec", "فعال سازی کدک سخت افزاری"), + ("Unlock Security Settings", "آنلاک شدن تنظیمات امنیتی"), + ("Enable Audio", "فعال شدن صدا"), + ("Unlock Network Settings", "آنلاک شدن تنظیمات شبکه"), ("Server", "سرور"), - ("Direct IP Access", "دسترسی مستقیم به IP"), + ("Direct IP Access", "IP دسترسی مستقیم "), ("Proxy", "پروکسی"), - ("Port", "پورت"), ("Apply", "ثبت"), - ("Disconnect all devices?", "همه دستگاه ها را غیرفعال کنید؟"), + ("Disconnect all devices?", "همه دستگاه ها قطع شوند؟"), ("Clear", "پاک کردن"), ("Audio Input Device", "منبع صدا"), ("Deny remote access", "دسترسی از راه دور را رد کنید"), - ("Use IP Whitelisting", "از لیست سفید IP استفاده کنید"), + ("Use IP Whitelisting", "های مجاز IP استفاده از"), ("Network", "شبکه"), - ("Enable RDP", "RDP را فعال کنید"), - ("Pin menubar", "نوار منو ثابت کنید"), - ("Unpin menubar", "پین نوار منو را بردارید"), + ("Enable RDP", "RDP فعال شدن"), + ("Pin menubar", "پین کردن نوار منو"), + ("Unpin menubar", "آنپین کردن نوار منو"), ("Recording", "در حال ضبط"), ("Directory", "مسیر"), ("Automatically record incoming sessions", "ضبط خودکار جلسات ورودی"), @@ -363,7 +368,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start session recording", "شروع ضبط جلسه"), ("Stop session recording", "توقف ضبط جلسه"), ("Enable Recording Session", "فعالسازی ضبط جلسه"), - ("Allow recording session", "مجوز ضبط جلسه"), + ("Allow recording session", "مجومجاز بودن ضبط جلسه"), ("Enable LAN Discovery", "فعالسازی جستجو در شبکه"), ("Deny LAN Discovery", "غیر فعالسازی جستجو در شبکه"), ("Write a message", "یک پیام بنویسید"), @@ -371,26 +376,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please wait for confirmation of UAC...", ""), ("elevated_foreground_window_tip", ""), ("Disconnected", "قطع ارتباط"), - ("Other", "دیگر"), - ("Confirm before closing multiple tabs", "بستن چندین برگه را تأیید کنید"), + ("Other", "سایر"), + ("Confirm before closing multiple tabs", "تایید بستن دسته ای برگه ها"), ("Keyboard Settings", "تنظیمات صفحه کلید"), - ("Custom", "سفارشی"), ("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.", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"), ("JumpLink", ""), ("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحه‌ای را برای اشتراک‌گذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."), - ("Show RustDesk", "RustDesk را نشان دهید"), + ("Show RustDesk", "RustDesk نمایش"), ("This PC", "This PC"), ("or", "یا"), ("Continue with", "ادامه با"), - ("Elevate", ""), - ("Zoom cursor", "نشانگر بزرگنمایی"), + ("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...", "لطفا صبر کنید تا میزبان درخواست شما را قبول کند..."), + ("Please wait for the remote side to accept your session request...", "...لطفا صبر کنید تا میزبان درخواست شما را قبول کند"), ("One-time Password", "رمز عبور یکبار مصرف"), ("Use one-time password", "استفاده از رمز عبور یکبار مصرف"), ("One-time password length", "طول رمز عبور یکبار مصرف"), @@ -399,6 +403,15 @@ 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", ""), ("Add to Address Book", "افزودن به دفترچه آدرس"), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index c3d241bf8..499be7c54 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Changer d'ID"), ("Website", "Site Web"), ("About", "À propos de"), + ("Slogan_tip", "Fait avec cœur dans ce monde chaotique!"), + ("Privacy Statement", "Déclaration de confidentialité"), ("Mute", "Muet"), ("Audio Input", "Entrée audio"), ("Enhancements", "Améliorations"), @@ -172,7 +174,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnect", "Déconnecter"), ("Allow using keyboard and mouse", "Autoriser l'utilisation du clavier et de la souris"), ("Allow using clipboard", "Autoriser l'utilisation du presse-papier"), - ("Allow hearing sound", "Autoriser l'audition du son"), + ("Allow hearing sound", "Autoriser l'envoi du son"), ("Allow file copy and paste", "Autoriser le copier-coller de fichiers"), ("Connected", "Connecté"), ("Direct and encrypted connection", "Connexion directe chiffrée"), @@ -181,7 +183,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and unencrypted connection", "Connexion relais non chiffrée"), ("Enter Remote ID", "Entrer l'ID de l'appareil à distance"), ("Enter your password", "Entrer votre mot de passe"), - ("Logging in...", "Se connecter..."), + ("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"), @@ -194,7 +196,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), - ("Reboot required", "Redémarrage pour prendre effet"), + ("Reboot required", "Redémarrage requis"), ("Unsupported display server ", "Le serveur d'affichage actuel n'est pas pris en charge"), ("x11 expected", "x11 requis"), ("Port", "Port"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Forcer la connexion relais"), ("whitelist_tip", "Seul l'IP dans la liste blanche peut accéder à mon appareil"), ("Login", "Connexion"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Déconnexion"), ("Tags", "Étiqueter"), ("Search ID", "Rechercher un ID"), @@ -217,11 +224,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add Tag", "Ajouter une balise"), ("Unselect all tags", "Désélectionner toutes les balises"), ("Network error", "Erreur réseau"), - ("Username missed", "Nom d'utilisateur manqué"), - ("Password missed", "Mot de passe manqué"), + ("Username missed", "Nom d'utilisateur manquant"), + ("Password missed", "Mot de passe manquant"), ("Wrong credentials", "Identifiant ou mot de passe erroné"), ("Edit Tag", "Modifier la balise"), - ("Unremember Password", "Mot de passe oublié"), + ("Unremember Password", "Oublier le Mot de passe"), ("Favorites", "Favoris"), ("Add to Favorites", "Ajouter aux Favoris"), ("Remove from Favorites", "Retirer des favoris"), @@ -316,7 +323,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Exit Fullscreen", "Quitter le mode plein écran"), ("Fullscreen", "Plein écran"), ("Mobile Actions", "Actions mobiles"), - ("Select Monitor", "Sélectionnez Moniteur"), + ("Select Monitor", "Sélection du Moniteur"), ("Control Actions", "Actions de contrôle"), ("Display Settings", "Paramètres d'affichage"), ("Ratio", "Rapport"), @@ -328,11 +335,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Connexion relais"), ("Secure Connection", "Connexion sécurisée"), ("Insecure Connection", "Connexion non sécurisée"), - ("Scale original", "Échelle d'origine"), - ("Scale adaptive", "Échelle adaptative"), + ("Scale original", "Échelle 100%"), + ("Scale adaptive", "Mise à l'échelle Auto"), ("General", "Général"), ("Security", "Sécurité"), - ("Account", "Compte"), ("Theme", "Thème"), ("Dark Theme", "Thème somble"), ("Dark", "Sombre"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Serveur"), ("Direct IP Access", "Accès IP direct"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Appliquer"), ("Disconnect all devices?", "Déconnecter tous les appareils"), ("Clear", "Effacer"), @@ -360,8 +365,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Directory", "Répertoire"), ("Automatically record incoming sessions", "Enregistrement automatique des session entrantes"), ("Change", "Modifier"), - ("Start session recording", "Commerce l'enregistrement"), - ("Stop session recording", "Stoper l'enregistrement"), + ("Start session recording", "Commencer l'enregistrement"), + ("Stop session recording", "Stopper l'enregistrement"), ("Enable Recording Session", "Activer l'enregistrement de session"), ("Allow recording session", "Autoriser l'enregistrement de session"), ("Enable LAN Discovery", "Activer la découverte réseau local"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Divers"), ("Confirm before closing multiple tabs", "Confirmer avant de fermer plusieurs onglets"), ("Keyboard Settings", "Configuration clavier"), - ("Custom", "Personnalisé"), ("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."), @@ -397,8 +401,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), - ("Right click to select tabs", ""), - ("Add to Address Book", ""), + ("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"), + ("Skipped", "Ignoré"), + ("Add to Address Book", "Ajouter au carnet d'adresses"), + ("Group", "Groupe"), + ("Search", "Rechercher"), + ("Closed manually by the web console", "Fermé 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", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index ecabd8f31..53369a4b3 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Αλλαγή αναγνωριστικού ID"), ("Website", "Ιστότοπος"), ("About", "Πληροφορίες"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Σίγαση"), ("Audio Input", "Είσοδος ήχου"), ("Enhancements", "Βελτιώσεις"), @@ -121,8 +123,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Εμφάνιση παρακολούθησης ποιότητας σύνδεσης"), ("Disable clipboard", "Απενεργοποίηση προχείρου"), ("Lock after session end", "Κλείδωμα μετά το τέλος της συνεδρίας"), - ("Insert", "Εισάγετε"), - ("Insert Lock", "Εισαγωγή κλειδαριάς"), + ("Insert", ""), + ("Insert Lock", "Κλείδωμα απομακρυσμένου σταθμού"), ("Refresh", "Ανανέωση"), ("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"), ("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με διακομιστή"), @@ -179,7 +181,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and encrypted connection", "Κρυπτογραφημένη σύνδεση με αναμετάδοση"), ("Direct and unencrypted connection", "Άμεση και μη κρυπτογραφημένη σύνδεση"), ("Relayed and unencrypted connection", "Μη κρυπτογραφημένη σύνδεση με αναμετάδοση"), - ("Enter Remote ID", "Εισαγωγή απομακρυσμένου αναγνωριστικού ID"), + ("Enter Remote ID", "Εισαγωγή απομακρυσμένου ID"), ("Enter your password", "Εισάγετε τον κωδικό σας"), ("Logging in...", "Σύνδεση..."), ("Enable RDP session sharing", "Ενεργοποίηση κοινής χρήσης RDP"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Σύνδεση πάντα μέσω αναμετάδοσης"), ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"), ("Login", "Σύνδεση"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Αποσύνδεση"), ("Tags", "Ετικέτες"), ("Search ID", "Αναζήτηση ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Προσαρμοστική κλίμακα"), ("General", "Γενικά"), ("Security", "Ασφάλεια"), - ("Account", "Λογαριασμός"), ("Theme", "Θέμα"), ("Dark Theme", "Σκούρο θέμα"), ("Dark", "Σκούρο"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Διακομιστής"), ("Direct IP Access", "Άμεση πρόσβαση IP"), ("Proxy", "Διαμεσολαβητής"), - ("Port", "Θύρα"), ("Apply", "Εφαρμογή"), ("Disconnect all devices?", "Αποσύνδεση όλων των συσκευών;"), ("Clear", "Καθαρισμός"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Άλλα"), ("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσετε πολλές καρτέλες"), ("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"), - ("Custom", "Προσαρμογή ποιότητας εικόνας"), ("Full Access", "Πλήρης πρόσβαση"), ("Screen Share", "Κοινή χρήση οθόνης"), ("Wayland requires Ubuntu 21.04 or higher version.", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), ("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."), ("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"), - ("Add to Address Book", ""), + ("Skipped", ""), + ("Add to Address Book", "Προσθήκη στο Βιβλίο Διευθύνσεων"), + ("Group", "Ομάδα"), + ("Search", "Αναζήτηση"), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index d0f2f4412..32d920994 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Azonosító megváltoztatása"), ("Website", "Weboldal"), ("About", "Rólunk"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Némítás"), ("Audio Input", "Hangátvitel"), ("Enhancements", "Fejlesztések"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Mindig közvetítőn keresztüli csatlakozás"), ("whitelist_tip", "Csak az engedélyezési listán szereplő címek csatlakozhatnak"), ("Login", "Belépés"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Kilépés"), ("Tags", "Tagok"), ("Search ID", "Azonosító keresése..."), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Adaptív méretarány"), ("General", "Általános"), ("Security", "Biztonság"), - ("Account", "Fiók"), ("Theme", "Téma"), ("Dark Theme", "Sötét téma"), ("Dark", "Sötét"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Szerver"), ("Direct IP Access", "Közvetlen IP hozzáférés"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Alkalmaz"), ("Disconnect all devices?", "Leválasztja az összes eszközt?"), ("Clear", "Tisztítás"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Egyéb"), ("Confirm before closing multiple tabs", "Biztos, hogy bezárja az összes lapot?"), ("Keyboard Settings", "Billentyűzet beállítások"), - ("Custom", "Egyedi"), ("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."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index b8f9e392d..c33cccb66 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -15,7 +15,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("not_ready_status", "Belum siap. Silakan periksa koneksi Anda"), ("Control Remote Desktop", "Kontrol Remote Desktop"), ("Transfer File", "File Transfer"), - ("Connect", "Menghubung"), + ("Connect", "Terhubung"), ("Recent Sessions", "Sesi Terkini"), ("Address Book", "Buku Alamat"), ("Confirmation", "Konfirmasi"), @@ -30,15 +30,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("IP Whitelisting", "Daftar Putih IP"), ("ID/Relay Server", "ID/Relay Server"), ("Import Server Config", "Impor Konfigurasi Server"), - ("Export Server Config", ""), + ("Export Server Config", "Ekspor Konfigutasi Server"), ("Import server configuration successfully", "Impor konfigurasi server berhasil"), - ("Export server configuration successfully", ""), + ("Export server configuration successfully", "Ekspor konfigurasi server berhasil"), ("Invalid server configuration", "Konfigurasi server tidak valid"), ("Clipboard is empty", "Papan klip kosong"), ("Stop service", "Hentikan Layanan"), ("Change ID", "Ubah ID"), ("Website", "Website"), ("About", "Tentang"), + ("Slogan_tip", ""), + ("Privacy Statement", "Pernyataan Privasi"), ("Mute", "Bisukan"), ("Audio Input", "Masukkan Audio"), ("Enhancements", "Peningkatan"), @@ -58,16 +60,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Skip", "Lanjutkan"), ("Close", "Tutup"), ("Retry", "Ulangi"), - ("OK", "OK"), - ("Password Required", "Password dibutukan"), - ("Please enter your password", "Silahkan masukkan password anda"), + ("OK", "Oke"), + ("Password Required", "Kata sandi dibutuhkan"), + ("Please enter your password", "Silahkan masukkan kata sandi anda"), ("Remember password", "Ingat Password"), - ("Wrong Password", "Password Salah"), + ("Wrong Password", "Kata sandi Salah"), ("Do you want to enter again?", "Apakah anda ingin masuk lagi?"), ("Connection Error", "Kesalahan koneksi"), ("Error", "Kesalahan"), ("Reset by the peer", "Setel ulang oleh rekan"), - ("Connecting...", "Hubungkan..."), + ("Connecting...", "Menghubungkan..."), ("Connection in progress. Please wait.", "Koneksi sedang berlangsung. Mohon tunggu."), ("Please try 1 minute later", "Silahkan coba 1 menit lagi"), ("Login Error", "Kesalahan Login"), @@ -108,7 +110,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Blokir masukan pengguna"), ("Unblock user input", "Jangan blokir masukan pengguna"), ("Adjust Window", "Sesuaikan Jendela"), - ("Original", "Original"), + ("Original", "Asli"), ("Shrink", "Susutkan"), ("Stretch", "Regangkan"), ("Scrollbar", "Scroll bar"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Selalu terhubung melalui relai"), ("whitelist_tip", "Hanya whitelisted IP yang dapat mengakses saya"), ("Login", "Masuk"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Keluar"), ("Tags", "Tag"), ("Search ID", "Cari ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skala adaptif"), ("General", "Umum"), ("Security", "Keamanan"), - ("Account", "Akun"), ("Theme", "Tema"), ("Dark Theme", "Tema gelap"), ("Dark", "Gelap"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direct IP Access"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Terapkan"), ("Disconnect all devices?", "Putuskan sambungan semua perangkat?"), ("Clear", ""), @@ -374,31 +379,39 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Lainnya"), ("Confirm before closing multiple tabs", "Konfirmasi sebelum menutup banyak tab"), ("Keyboard Settings", "Pengaturan Papan Ketik"), - ("Custom", "Kustom"), ("Full Access", "Akses penuh"), ("Screen Share", "Berbagi Layar"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland membutuhkan Ubuntu 21.04 atau versi yang lebih tinggi."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland membutuhkan versi distro linux yang lebih tinggi. Silakan coba desktop X11 atau ubah OS Anda."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Silakan Pilih layar yang akan dibagikan (Operasi di sisi rekan)."), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), + ("Show RustDesk", "Tampilkan RustDesk"), + ("This PC", "PC ini"), + ("or", "atau"), + ("Continue with", "Lanjutkan dengan"), ("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", ""), + ("Accept sessions via password", "Izinkan sesi dengan kata sandi"), + ("Accept sessions via click", "Izinkan sesi dengan klik"), + ("Accept sessions via both", "Izinkan sesi dengan keduanya"), + ("Please wait for the remote side to accept your session request...", "Harap tunggu sisi jarak jauh untuk menerima permintaan sesi Anda..."), + ("One-time Password", "Kata sandi satu kali"), + ("Use one-time password", "Gunakan kata sandi satu kali"), ("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", "Pencarian"), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index b7d449a62..ac3ea46fa 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Cambia ID"), ("Website", "Sito web"), ("About", "Informazioni"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Silenzia"), ("Audio Input", "Input audio"), ("Enhancements", "Miglioramenti"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Connetti sempre tramite relay"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("Login", "Accedi"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Esci"), ("Tags", "Tag"), ("Search ID", "Cerca ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Scala adattiva"), ("General", "Generale"), ("Security", "Sicurezza"), - ("Account", "Account"), ("Theme", "Tema"), ("Dark Theme", "Tema Scuro"), ("Dark", "Scuro"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", "Accesso IP diretto"), ("Proxy", ""), - ("Port", "Porta"), ("Apply", "Applica"), ("Disconnect all devices?", "Disconnettere tutti i dispositivi?"), ("Clear", "Ripulisci"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Altro"), ("Confirm before closing multiple tabs", "Conferma prima di chiudere più schede"), ("Keyboard Settings", "Impostazioni tastiera"), - ("Custom", "Personalizzato"), ("Full Access", "Accesso completo"), ("Screen Share", "Condivisione dello schermo"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland richiede Ubuntu 21.04 o successiva."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Permetti di nascondere solo se si accettano sessioni con password permanente"), ("wayland_experiment_tip", "Il supporto Wayland è in fase sperimentale, utilizza X11 se necessiti di un accesso stabile."), ("Right click to select tabs", "Clic con il tasto destro per selezionare le schede"), + ("Skipped", "Saltato"), ("Add to Address Book", "Aggiungi alla rubrica"), + ("Group", "Gruppo"), + ("Search", "Cerca"), + ("Closed manually by the web console", "Chiudi manualmente dalla console Web"), + ("Local keyboard type", "Tipo di tastiera locale"), + ("Select local keyboard type", "Seleziona il tipo di tastiera locale"), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 4ca33e76a..7dd1640f6 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "IDを変更"), ("Website", "公式サイト"), ("About", "情報"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "ミュート"), ("Audio Input", "音声入力デバイス"), ("Enhancements", "追加機能"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "常に中継サーバー経由で接続"), ("whitelist_tip", "ホワイトリストに登録されたIPからのみ接続を許可します"), ("Login", "ログイン"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "ログアウト"), ("Tags", "タグ"), ("Search ID", "IDを検索"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "フィットウィンドウ"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "他の"), ("Confirm before closing multiple tabs", "同時に複数のタブを閉じる前に確認する"), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland には、Ubuntu 21.04 以降のバージョンが必要です。"), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 93338165b..66ff3ca95 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID 변경"), ("Website", "웹사이트"), ("About", "정보"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "음소거"), ("Audio Input", "오디오 입력"), ("Enhancements", ""), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "항상 relay를 통해 접속하기"), ("whitelist_tip", "화이트리스트에 있는 IP만 현 데스크탑에 접속 가능합니다"), ("Login", "로그인"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "로그아웃"), ("Tags", "태그"), ("Search ID", "ID 검색"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "맞는 창"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index a7d6f299d..ac688eb9f 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID ауыстыру"), ("Website", "Web-сайт"), ("About", "Туралы"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Дыбыссыздандыру"), ("Audio Input", "Аудио Еңгізу"), ("Enhancements", "Жақсартулар"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), ("Login", "Кіру"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Шығу"), ("Tags", "Тақтар"), ("Search ID", "ID Іздеу"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Scale adaptive"), ("General", ""), ("Security", ""), - ("Account", "Есепкі"), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", "Порт"), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland Ubuntu 21.04 немесе одан жоғары нұсқасын қажет етеді."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index d3f991d44..afd6b4b03 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), ("Your Desktop", "Twój pulpit"), - ("desk_tip", "Aby połaczyć się z tym urządzeniem należy użyć tego ID i hasła."), + ("desk_tip", "W celu zestawienia połączenia z tym urządzeniem należy poniższego ID i hasła."), ("Password", "Hasło"), ("Ready", "Gotowe"), ("Established", "Nawiązano"), @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Usługa uruchomiona"), ("Service is not running", "Usługa nie jest uruchomiona"), ("not_ready_status", "Brak gotowości"), - ("Control Remote Desktop", "Kontroluj zdalny pulpit"), + ("Control Remote Desktop", "Połącz się z"), ("Transfer File", "Transfer plików"), ("Connect", "Połącz"), ("Recent Sessions", "Ostatnie sesje"), @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Zmień ID"), ("Website", "Strona internetowa"), ("About", "O"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Wycisz"), ("Audio Input", "Wejście audio"), ("Enhancements", "Ulepszenia"), @@ -118,7 +120,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Optimize reaction time", "Zoptymalizuj czas reakcji"), ("Custom", "Własne"), ("Show remote cursor", "Pokazuj zdalny kursor"), - ("Show quality monitor", "Pokazuj jakość monitora"), + ("Show quality monitor", "Parametry połączenia"), ("Disable clipboard", "Wyłącz schowek"), ("Lock after session end", "Zablokuj po zakończeniu sesji"), ("Insert", "Wyślij"), @@ -137,9 +139,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set Password", "Ustaw hasło"), ("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ęcią problemów z UAC, kliknij poniższy przycisk by zainstalować RustDesk w swoim systemie."), - ("Click to upgrade", "Kliknij, aby zaktualizować (upgrade)"), - ("Click to download", "Kliknij, aby pobrać"), - ("Click to update", "Kliknij, aby zaktualizować (update)"), + ("Click to upgrade", "Zaktualizuj"), + ("Click to download", "Pobierz"), + ("Click to update", "Uaktualinij"), ("Configure", "Konfiguruj"), ("config_acc", "Konfiguracja konta"), ("config_screen", "Konfiguracja ekranu"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Zawsze łącz pośrednio"), ("whitelist_tip", "Zezwlaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), ("Login", "Zaloguj"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Wyloguj"), ("Tags", "Tagi"), ("Search ID", "Szukaj ID"), @@ -314,25 +321,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote_restarting_tip", "Trwa ponownie uruchomienie zdalnego urządzenia, zamknij ten komunikat i ponownie nawiąż za chwilę połączenie używając hasła permanentnego"), ("Copied", "Skopiowano"), ("Exit Fullscreen", "Wyłączyć tryb pełnoekranowy"), - ("Fullscreen", "Pełny ekran"), - ("Mobile Actions", "Działania mobilne"), - ("Select Monitor", "Wybierz Monitor"), - ("Control Actions", "Działania kontrolne"), + ("Fullscreen", "Tryb pełnoekranowy"), + ("Mobile Actions", "Dostępne mobilne polecenia"), + ("Select Monitor", "Wybierz ekran"), + ("Control Actions", "Dostępne polecenia"), ("Display Settings", "Ustawienia wyświetlania"), ("Ratio", "Proporcje"), ("Image Quality", "Jakość obrazu"), ("Scroll Style", "Styl przewijania"), ("Show Menubar", "Pokaż pasek menu"), ("Hide Menubar", "Ukryj pasek menu"), - ("Direct Connection", "Połącznie Bezpośrednie"), - ("Relay Connection", "Połączenie Pośrednie"), - ("Secure Connection", "Połączenie Bezpieczne"), - ("Insecure Connection", "Połączenie Niebezpieczne"), + ("Direct Connection", "Połącznie bezpośrednie"), + ("Relay Connection", "Połączenie przez bramkę"), + ("Secure Connection", "Połączenie szyfrowane"), + ("Insecure Connection", "Połączenie nieszyfrowane"), ("Scale original", "Skaluj oryginalnie"), ("Scale adaptive", "Skaluj adaptacyjnie"), ("General", "Ogólne"), ("Security", "Zabezpieczenia"), - ("Account", "Konto"), ("Theme", "Motyw"), ("Dark Theme", "Ciemny motyw"), ("Dark", "Ciemny"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Serwer"), ("Direct IP Access", "Bezpośredni Adres IP"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Zastosuj"), ("Disconnect all devices?", "Czy rozłączyć wszystkie urządzenia?"), ("Clear", "Wyczyść"), @@ -364,17 +369,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop session recording", "Zatrzymaj nagrywanie sesji"), ("Enable Recording Session", "Włącz Nagrywanie Sesji"), ("Allow recording session", "Zezwól na nagrywanie sesji"), - ("Enable LAN Discovery", "Włącz Wykrywanie LAN"), - ("Deny LAN Discovery", "Zablokuj Wykrywanie LAN"), + ("Enable LAN Discovery", "Włącz wykrywanie urządzenia w sieci LAN"), + ("Deny LAN Discovery", "Zablokuj wykrywanie urządzenia w sieci LAN"), ("Write a message", "Napisz wiadomość"), ("Prompt", "Monit"), - ("Please wait for confirmation of UAC...", ""), + ("Please wait for confirmation of UAC...", "Oczekuje potwierdzenia ustawień UAC"), ("elevated_foreground_window_tip", ""), ("Disconnected", "Rozłączone"), ("Other", "Inne"), ("Confirm before closing multiple tabs", "Potwierdź przed zamknięciem wielu kart"), ("Keyboard Settings", "Ustawienia klawiatury"), - ("Custom", "Własne"), ("Full Access", "Pełny dostęp"), ("Screen Share", "Udostępnianie ekranu"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland wymaga Ubuntu 21.04 lub nowszego."), @@ -385,20 +389,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "Ten komputer"), ("or", "albo"), ("Continue with", "Kontynuuj z"), - ("Elevate", "Podwyższ"), + ("Elevate", "Uzyskaj uprawnienia"), ("Zoom cursor", "Zoom kursora"), - ("Accept sessions via password", "Akceptuj sesje używając hasła"), - ("Accept sessions via click", "Akceptuj sesję klikając"), - ("Accept sessions via both", "Akceptuj sesjęna dwa sposoby"), - ("Please wait for the remote side to accept your session request...", "Proszę czekać aż zdalny host zaakceptuje Twoją prośbę..."), + ("Accept sessions via password", "Uwierzytelnij sesję używając hasła"), + ("Accept sessions via click", "Uwierzytelnij sesję poprzez kliknięcie"), + ("Accept sessions via both", "Uwierzytelnij sesję za pomocą obu sposobów"), + ("Please wait for the remote side to accept your session request...", "Oczekiwanie, na zatwierdzenie sesji przez host zdalny..."), ("One-time Password", "Hasło jednorazowe"), ("Use one-time password", "Użyj hasła jednorazowego"), ("One-time password length", "Długość hasła jednorazowego"), ("Request access to your device", "Żądanie dostępu do Twojego urządzenia"), - ("Hide connection management window", ""), + ("Hide connection management window", "Ukryj okno zarządzania połączeniem"), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), - ("Add to Address Book", ""), + ("Skipped", ""), + ("Add to Address Book", "Dodaj do Książki Adresowej"), + ("Group", "Grypy"), + ("Search", "Szukaj"), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 4a457218c..bf7954b46 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Alterar ID"), ("Website", "Website"), ("About", "Sobre"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs na whitelist podem me acessar"), ("Login", "Login"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Sair"), ("Tags", "Tags"), ("Search ID", "Procurar ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptável"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Outro"), ("Confirm before closing multiple tabs", "Confirme antes de fechar vários separadores"), ("Keyboard Settings", "Configurações do teclado"), - ("Custom", ""), ("Full Access", "Controlo total"), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requer Ubuntu 21.04 ou versão superior."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index af59e4f2e..207be548f 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Alterar ID"), ("Website", "Website"), ("About", "Sobre"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Desativar som"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs confiáveis podem me acessar"), ("Login", "Login"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Sair"), ("Tags", "Tags"), ("Search ID", "Pesquisar ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptada"), ("General", "Geral"), ("Security", "Segurança"), - ("Account", "Conta"), ("Theme", "Tema"), ("Dark Theme", "Tema escuro"), ("Dark", "Escuro"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Servidor"), ("Direct IP Access", "Acesso direto por IP"), ("Proxy", "Proxy"), - ("Port", "Porta"), ("Apply", "Aplicar"), ("Disconnect all devices?", "Desconectar todos os dispositivos?"), ("Clear", "Limpar"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Outro"), ("Confirm before closing multiple tabs", "Confirmar antes de fechar múltiplas abas"), ("Keyboard Settings", "Configurações de teclado"), - ("Custom", "Personalizado"), ("Full Access", "Acesso completo"), ("Screen Share", "Compartilhamento de tela"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requer Ubuntu 21.04 ou versão superior."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 8d990fc66..6a9d2f297 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Изменить ID"), ("Website", "Сайт"), ("About", "О программе"), + ("Slogan_tip", "Сделано с душой в этом безумном мире!"), + ("Privacy Statement", "Заявление о конфиденциальности"), ("Mute", "Отключить звук"), ("Audio Input", "Аудиовход"), ("Enhancements", "Улучшения"), @@ -204,10 +206,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Закрыто удалённым узлом вручную"), ("Enable remote configuration modification", "Разрешить удалённое изменение конфигурации"), ("Run without install", "Запустить без установки"), - ("Always connected via relay", "Всегда подключён через ретрансляционный сервер"), + ("Always connected via relay", "Всегда подключается через ретрансляционный сервер"), ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("Login", "Войти"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Выйти"), ("Tags", "Метки"), ("Search ID", "Поиск по ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Адаптивный масштаб"), ("General", "Общие"), ("Security", "Безопасность"), - ("Account", "Аккаунт"), ("Theme", "Тема"), ("Dark Theme", "Тёмная тема"), ("Dark", "Тёмная"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Сервер"), ("Direct IP Access", "Прямой IP-доступ"), ("Proxy", "Прокси"), - ("Port", "Порт"), ("Apply", "Применить"), ("Disconnect all devices?", "Отключить все устройства?"), ("Clear", "Очистить"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Другое"), ("Confirm before closing multiple tabs", "Подтверждать закрытие несколько вкладок"), ("Keyboard Settings", "Настройки клавиатуры"), - ("Custom", "Своё"), ("Full Access", "Полный доступ"), ("Screen Share", "Поделиться экраном"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland требует Ubuntu 21.04 или более позднюю версию."), @@ -399,6 +403,15 @@ 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", "Пропущено"), ("Add to Address Book", "Добавить в адресную книгу"), + ("Group", "Группа"), + ("Search", "Поиск"), + ("Closed manually by the web console", "Закрыто вручную через веб-консоль"), + ("Local keyboard type", "Тип локальной клавиатуры"), + ("Select local keyboard type", "Выберите тип локальной клавиатуры"), + ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), + ("Always use software rendering", "Использовать программную визуализацию"), + ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 13672d086..40f19c625 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Zmeniť ID"), ("Website", "Webová stránka"), ("About", "O RustDesk"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Stíšiť"), ("Audio Input", "Zvukový vstup"), ("Enhancements", ""), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Vždy pripájať cez prepájací server"), ("whitelist_tip", "Len vymenované IP adresy majú oprávnenie sa pripojiť k vzdialenej správe"), ("Login", "Prihlásenie"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Odhlásenie"), ("Tags", "Štítky"), ("Search ID", "Hľadať ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Prispôsobivá mierka"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04 alebo vyššiu verziu."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs new file mode 100755 index 000000000..5e8efc17d --- /dev/null +++ b/src/lang/sl.rs @@ -0,0 +1,417 @@ +lazy_static::lazy_static! { +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"), + ("Password", "Geslo"), + ("Ready", "Pripravljen"), + ("Established", "Povezava vzpostavljena"), + ("connecting_status", "Vzpostavljanje povezave z omrežjem RustDesk..."), + ("Enable Service", "Omogoči storitev"), + ("Start Service", "Zaženi storitev"), + ("Service is running", "Storitev se izvaja"), + ("Service is not running", "Storitev se ne izvaja"), + ("not_ready_status", "Ni pripravljeno, preverite vašo mrežno povezavo"), + ("Control Remote Desktop", "Nadzoruj oddaljeno namizje"), + ("Transfer File", "Prenos datotek"), + ("Connect", "Poveži"), + ("Recent Sessions", "Nedavne seje"), + ("Address Book", "Adresar"), + ("Confirmation", "Potrditev"), + ("TCP Tunneling", "TCP tuneliranje"), + ("Remove", "Odstrani"), + ("Refresh random password", "Osveži naključno geslo"), + ("Set your own password", "Nastavi lastno geslo"), + ("Enable Keyboard/Mouse", "Omogoči tipkovnico in miško"), + ("Enable Clipboard", "Omogoči odložišče"), + ("Enable File Transfer", "Omogoči prenos datotek"), + ("Enable TCP Tunneling", "Omogoči TCP tuneliranje"), + ("IP Whitelisting", "Omogoči seznam dovoljenih IPjev"), + ("ID/Relay Server", "Strežnik za ID/posredovanje"), + ("Import Server Config", "Uvozi nastavitve strežnika"), + ("Export Server Config", "Izvozi nastavitve strežnika"), + ("Import server configuration successfully", "Nastavitve strežnika uspešno uvožene"), + ("Export server configuration successfully", "Nastavitve strežnika uspešno izvožene"), + ("Invalid server configuration", "Neveljavne nastavitve strežnika"), + ("Clipboard is empty", "Odložišče je prazno"), + ("Stop service", "Ustavi storitev"), + ("Change ID", "Spremeni ID"), + ("Website", "Spletna stran"), + ("About", "O programu"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Izklopi zvok"), + ("Audio Input", "Avdio vhod"), + ("Enhancements", "Izboljšave"), + ("Hardware Codec", "Strojni kodek"), + ("Adaptive Bitrate", "Prilagodljiva bitna hitrost"), + ("ID Server", "ID strežnik"), + ("Relay Server", "Posredniški strežnik"), + ("API Server", "API strežnik"), + ("invalid_http", "mora se začeti s http:// ali https://"), + ("Invalid IP", "Neveljaven IP"), + ("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."), + ("Invalid format", "Neveljavna oblika"), + ("server_not_support", "Strežnik še ne podpira"), + ("Not available", "Ni na voljo"), + ("Too frequent", "Prepogosto"), + ("Cancel", "Prekliči"), + ("Skip", "Izpusti"), + ("Close", "Zapri"), + ("Retry", "Ponovi"), + ("OK", "V redu"), + ("Password Required", "Potrebno je geslo"), + ("Please enter your password", "Vnesite vaše geslo"), + ("Remember password", "Zapomni si geslo"), + ("Wrong Password", "Napačno geslo"), + ("Do you want to enter again?", "Želite znova vnesti?"), + ("Connection Error", "Napaka pri povezavi"), + ("Error", "Napaka"), + ("Reset by the peer", "Povezava prekinjena"), + ("Connecting...", "Povezovanje..."), + ("Connection in progress. Please wait.", "Vzpostavljanje povezave, prosim počakajte."), + ("Please try 1 minute later", "Poizkusite čez 1 minuto"), + ("Login Error", "Napaka pri prijavi"), + ("Successful", "Uspešno"), + ("Connected, waiting for image...", "Povezava vzpostavljena, čakam na sliko..."), + ("Name", "Ime"), + ("Type", "Vrsta"), + ("Modified", "Čas spremembe"), + ("Size", "Velikost"), + ("Show Hidden Files", "Prikaži skrite datoteke"), + ("Receive", "Prejmi"), + ("Send", "Pošlji"), + ("Refresh File", "Osveži datoteko"), + ("Local", "Lokalno"), + ("Remote", "Oddaljeno"), + ("Remote Computer", "Oddaljeni računalnik"), + ("Local Computer", "Lokalni računalnik"), + ("Confirm Delete", "Potrdi izbris"), + ("Delete", "Izbriši"), + ("Properties", "Lastnosti"), + ("Multi Select", "Večkratna izbira"), + ("Select All", "Izberi vse"), + ("Unselect All", "Počisti vse"), + ("Empty Directory", "Prazen imenik"), + ("Not an empty directory", "Imenik ni prazen"), + ("Are you sure you want to delete this file?", "Ali res želite izbrisati to datoteko?"), + ("Are you sure you want to delete this empty directory?", "Ali res želite izbrisati to prazno mapo?"), + ("Are you sure you want to delete the file of this directory?", "Ali res želite datoteko iz mape?"), + ("Do this for all conflicts", "Naredi to za vse"), + ("This is irreversible!", "Tega dejanja ni mogoče razveljaviti!"), + ("Deleting", "Brisanje"), + ("files", "datoteke"), + ("Waiting", "Čakanje"), + ("Finished", "Opravljeno"), + ("Speed", "Hitrost"), + ("Custom Image Quality", "Kakovost slike po meri"), + ("Privacy mode", "Zasebni način"), + ("Block user input", "Onemogoči uporabnikov vnos"), + ("Unblock user input", "Omogoči uporabnikov vnos"), + ("Adjust Window", "Prilagodi okno"), + ("Original", "Originalno"), + ("Shrink", "Skrči"), + ("Stretch", "Raztegni"), + ("Scrollbar", "Drsenje z drsniki"), + ("ScrollAuto", "Samodejno drsenje"), + ("Good image quality", "Visoka kakovost slike"), + ("Balanced", "Uravnoteženo"), + ("Optimize reaction time", "Optimiraj odzivni čas"), + ("Custom", "Po meri"), + ("Show remote cursor", "Prikaži oddaljeni kazalec miške"), + ("Show quality monitor", "Prikaži nadzornik kakovosti"), + ("Disable clipboard", "Onemogoči odložišče"), + ("Lock after session end", "Zakleni ob koncu seje"), + ("Insert", "Vstavi"), + ("Insert Lock", "Zakleni oddaljeni računalnik"), + ("Refresh", "Osveži"), + ("ID does not exist", "ID ne obstaja"), + ("Failed to connect to rendezvous server", "Ni se bilo mogoče povezati na povezovalni strežnik"), + ("Please try later", "Poizkusite znova kasneje"), + ("Remote desktop is offline", "Oddaljeno namizje ni dosegljivo"), + ("Key mismatch", "Ključ ni ustrezen"), + ("Timeout", "Časovna omejitev"), + ("Failed to connect to relay server", "Ni se bilo mogoče povezati na posredniški strežnik"), + ("Failed to connect via rendezvous server", "Ni se bilo mogoče povezati preko povezovalnega strežnika"), + ("Failed to connect via relay server", "Ni se bilo mogoče povezati preko posredniškega strežnika"), + ("Failed to make direct connection to remote desktop", "Ni bilo mogoče vzpostaviti neposredne povezave z oddaljenim namizjem"), + ("Set Password", "Nastavi geslo"), + ("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"), + ("Installing ...", "Nameščanje..."), + ("Install", "Namesti"), + ("Installation", "Namestitev"), + ("Installation Path", "Pot za namestitev"), + ("Create start menu shortcuts", "Ustvari bližnjice v meniju Začetek"), + ("Create desktop icon", "Ustvari ikono na namizju"), + ("agreement_tip", "Z namestitvijo se strinjate z licenčno pogodbo"), + ("Accept and Install", "Sprejmi in namesti"), + ("End-user license agreement", "Licenčna pogodba za končnega uporabnika"), + ("Generating ...", "Ustvarjanje ..."), + ("Your installation is lower version.", "Vaša namestitev je starejša"), + ("not_close_tcp_tip", "Med uporabo tunela ne zaprite tega okna"), + ("Listening ...", "Poslušam ..."), + ("Remote Host", "Oddaljeni gostitelj"), + ("Remote Port", "Oddaljena vrata"), + ("Action", "Dejanje"), + ("Add", "Dodaj"), + ("Local Port", "Lokalna vrata"), + ("Local Address", "Lokalni naslov"), + ("Change Local Port", "Spremeni lokalna vrata"), + ("setup_server_tip", "Za hitrejšo povezavo uporabite lasten strežnik"), + ("Too short, at least 6 characters.", "Prekratek, mora biti najmanj 6 znakov."), + ("The confirmation is not identical.", "Potrditev ni enaka."), + ("Permissions", "Dovoljenja"), + ("Accept", "Sprejmi"), + ("Dismiss", "Opusti"), + ("Disconnect", "Prekini povezavo"), + ("Allow using keyboard and mouse", "Dovoli uporabo tipkovnice in miške"), + ("Allow using clipboard", "Dovoli uporabo odložišča"), + ("Allow hearing sound", "Dovoli prenos zvoka"), + ("Allow file copy and paste", "Dovoli kopiranje in lepljenje datotek"), + ("Connected", "Povezan"), + ("Direct and encrypted connection", "Neposredna šifrirana povezava"), + ("Relayed and encrypted connection", "Posredovana šifrirana povezava"), + ("Direct and unencrypted connection", "Neposredna nešifrirana povezava"), + ("Relayed and unencrypted connection", "Posredovana šifrirana povezava"), + ("Enter Remote ID", "Vnesi oddaljeni ID"), + ("Enter your password", "Vnesi geslo"), + ("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"), + ("Rename", "Preimenuj"), + ("Space", "Prazno"), + ("Create Desktop Shortcut", "Ustvari bližnjico na namizju"), + ("Change Path", "Spremeni pot"), + ("Create Folder", "Ustvari mapo"), + ("Please enter the folder name", "Vnesite ime mape"), + ("Fix it", "Popravi"), + ("Warning", "Opozorilo"), + ("Login screen using Wayland is not supported", "Prijava z Waylandom ni podprta"), + ("Reboot required", "Potreben je ponovni zagon"), + ("Unsupported display server ", "Nepodprt zaslonski strežnik"), + ("x11 expected", "Pričakovan X11"), + ("Port", "Vrata"), + ("Settings", "Nastavitve"), + ("Username", "Uporabniško ime"), + ("Invalid port", "Neveljavno geslo"), + ("Closed manually by the peer", "Povezavo ročno prekinil odjemalec"), + ("Enable remote configuration modification", "Omogoči oddaljeno spreminjanje nastavitev"), + ("Run without install", "Zaženi brez namestitve"), + ("Always connected via relay", "Vedno povezan preko posrednika"), + ("Always connect via relay", "Vedno poveži preko posrednika"), + ("whitelist_tip", "Dostop je možen samo iz dovoljenih IPjev"), + ("Login", "Prijavi"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), + ("Logout", "Odjavi"), + ("Tags", "Oznake"), + ("Search ID", "Išči ID"), + ("Current Wayland display server is not supported", "Trenutni Wayland zaslonski strežnik ni podprt"), + ("whitelist_sep", "Naslovi ločeni z vejico, podpičjem, presledkom ali novo vrstico"), + ("Add ID", "Dodaj ID"), + ("Add Tag", "Dodaj oznako"), + ("Unselect all tags", ""), + ("Network error", "Omrežna napaka"), + ("Username missed", "Up. ime izpuščeno"), + ("Password missed", "Geslo izpuščeno"), + ("Wrong credentials", "Napačne poverilnice"), + ("Edit Tag", "Uredi oznako"), + ("Unremember Password", "Pozabi geslo"), + ("Favorites", "Priljubljene"), + ("Add to Favorites", "Dodaj med priljubljene"), + ("Remove from Favorites", "Odstrani iz priljubljenih"), + ("Empty", "Prazno"), + ("Invalid folder name", "Napačno ime mape"), + ("Socks5 Proxy", "Socks5 posredniški strežnik"), + ("Hostname", "Ime gostitelja"), + ("Discovered", "Odkriti"), + ("install_daemon_tip", "Za samodejni zagon ob vklopu računalnika je potrebno dodati sistemsko storitev"), + ("Remote ID", "Oddaljeni ID"), + ("Paste", "Prilepi"), + ("Paste here?", "Prilepi tu?"), + ("Are you sure to close the connection?", "Ali želite prekiniti povezavo?"), + ("Download new version", "Prenesi novo različico"), + ("Touch mode", "Način dotika"), + ("Mouse mode", "Način mišle"), + ("One-Finger Tap", "Tap z enim prstom"), + ("Left Mouse", "Leva tipka miške"), + ("One-Long Tap", "Dolg tap z enim prstom"), + ("Two-Finger Tap", "Tap z dvema prstoma"), + ("Right Mouse", "Desna tipka miške"), + ("One-Finger Move", "Premik z enim prstom"), + ("Double Tap & Move", "Dvojni tap in premik"), + ("Mouse Drag", "Vlečenje z miško"), + ("Three-Finger vertically", "Triprstno navpično"), + ("Mouse Wheel", "Miškino kolesce"), + ("Two-Finger Move", "Premik z dvema prstoma"), + ("Canvas Move", "Premik platna"), + ("Pinch to Zoom", "Povečava s približevanjem prstov"), + ("Canvas Zoom", "Povečava platna"), + ("Reset canvas", "Ponastavi platno"), + ("No permission of file transfer", "Ni pravic za prenos datotek"), + ("Note", "Opomba"), + ("Connection", "Povezava"), + ("Share Screen", "Deli zaslon"), + ("CLOSE", "ZAPRI"), + ("OPEN", "ODPRI"), + ("Chat", "Pogovor"), + ("Total", "Skupaj"), + ("items", "elementi"), + ("Selected", "Izbrano"), + ("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?"), + ("android_input_permission_tip1", "Za oddaljeni nadzor vaše naprave Android, je potrebno RustDesku dodeliti pravico za dostopnost."), + ("android_input_permission_tip2", "Pojdite v sistemske nastavitve, poiščite »Nameščene storitve« in vklopite storitev »RustDesk Input«."), + ("android_new_connection_tip", "Prejeta je bila zahteva za oddaljeni nadzor vaše naprave."), + ("android_service_will_start_tip", "Z vklopom zajema zaslona se bo samodejno zagnala storitev, ki omogoča da oddaljene naprave pošljejo zahtevo za povezavo na vašo napravo."), + ("android_stop_service_tip", "Z zaustavitvijo storitve bodo samodejno prekinjene vse oddaljene povezave."), + ("android_version_audio_tip", "Trenutna različica Androida ne omogoča zajema zvoka. Za zajem zvoka nadgradite na Android 10 ali novejši."), + ("android_start_service_tip", "Tapnite »Zaženi storitev« ali »ODPRI« pri dovoljenju za zajem zaslona da zaženete storitev deljenja zaslona."), + ("Account", "Račun"), + ("Overwrite", "Prepiši"), + ("This file exists, skip or overwrite this file?", "Datoteka obstaja, izpusti ali prepiši?"), + ("Quit", "Izhod"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Pomoč"), + ("Failed", "Ni uspelo"), + ("Succeeded", "Uspelo"), + ("Someone turns on privacy mode, exit", "Vklopljen je zasebni način, izhod"), + ("Unsupported", "Ni podprto"), + ("Peer denied", "Odjemalec zavrnil"), + ("Please install plugins", "Namestite vključke"), + ("Peer exit", "Odjemalec se je zaprl"), + ("Failed to turn off", "Ni bilo mogoče izklopiti"), + ("Turned off", "Izklopljeno"), + ("In privacy mode", "V zasebnem načinu"), + ("Out privacy mode", "Iz zasebnega načina"), + ("Language", "Jezik"), + ("Keep RustDesk background service", "Ohrani RustDeskovo storitev v ozadju"), + ("Ignore Battery Optimizations", "Prezri optimizacije baterije"), + ("android_open_battery_optimizations_tip", "Če želite izklopiti to možnost, pojdite v nastavitve aplikacije RustDesk, poiščite »Baterija« in izklopite »Neomejeno«"), + ("Connection not allowed", "Povezava ni dovoljena"), + ("Legacy mode", "Stari način"), + ("Map mode", "Način preslikave"), + ("Translate mode", "Način prevajanja"), + ("Use permanent password", "Uporabi stalno geslo"), + ("Use both passwords", "Uporabi obe gesli"), + ("Set permanent password", "Nastavi stalno geslo"), + ("Enable Remote Restart", "Omogoči oddaljeni ponovni zagon"), + ("Allow remote restart", "Dovoli oddaljeni ponovni zagon"), + ("Restart Remote Device", "Znova zaženi oddaljeno napravo"), + ("Are you sure you want to restart", "Ali ste prepričani, da želite znova zagnati"), + ("Restarting Remote Device", "Ponovni zagon oddaljene naprave"), + ("remote_restarting_tip", "Oddaljena naprava se znova zaganja, prosim zaprite to sporočilo in se čez nekaj časa povežite s stalnim geslom."), + ("Copied", "Kopirano"), + ("Exit Fullscreen", "Izhod iz celozaslonskega načina"), + ("Fullscreen", "Celozaslonski način"), + ("Mobile Actions", "Dejanja za prenosne naprave"), + ("Select Monitor", "Izberite zaslon"), + ("Control Actions", "Dejanja za nadzor"), + ("Display Settings", "Nastavitve zaslona"), + ("Ratio", "Razmerje"), + ("Image Quality", "Kakovost slike"), + ("Scroll Style", "Način drsenja"), + ("Show Menubar", "Prikaži meni"), + ("Hide Menubar", "Skrij meni"), + ("Direct Connection", "Neposredna povezava"), + ("Relay Connection", "Posredovana povezava"), + ("Secure Connection", "Zavarovana povezava"), + ("Insecure Connection", "Nezavarovana povezava"), + ("Scale original", "Originalna velikost"), + ("Scale adaptive", "Prilagojena velikost"), + ("General", "Splošno"), + ("Security", "Varnost"), + ("Theme", "Tema"), + ("Dark Theme", "Temna tema"), + ("Dark", "Temna"), + ("Light", "Svetla"), + ("Follow System", "Sistemska"), + ("Enable hardware codec", "Omogoči strojno pospeševanje"), + ("Unlock Security Settings", "Odkleni varnostne nastavitve"), + ("Enable Audio", "Omogoči zvok"), + ("Unlock Network Settings", "Odkleni mrežne nastavitve"), + ("Server", "Strežnik"), + ("Direct IP Access", "Neposredni dostop preko IPja"), + ("Proxy", "Posredniški strežnik"), + ("Apply", "Uveljavi"), + ("Disconnect all devices?", "Odklopi vse naprave?"), + ("Clear", "Počisti"), + ("Audio Input Device", "Vhodna naprava za zvok"), + ("Deny remote access", "Onemogoči oddaljeni dostop"), + ("Use IP Whitelisting", "Omogoči seznam dovoljenih IP naslovov"), + ("Network", "Mreža"), + ("Enable RDP", "Omogoči RDP"), + ("Pin menubar", "Pripni menijsko vrstico"), + ("Unpin menubar", "Odpni menijsko vrstico"), + ("Recording", "Snemanje"), + ("Directory", "Imenik"), + ("Automatically record incoming sessions", "Samodejno snemaj vhodne seje"), + ("Change", "Spremeni"), + ("Start session recording", "Začni snemanje seje"), + ("Stop session recording", "Ustavi snemanje seje"), + ("Enable Recording Session", "Omogoči snemanje seje"), + ("Allow recording session", "Dovoli snemanje seje"), + ("Enable LAN Discovery", "Omogoči odkrivanje lokalnega omrežja"), + ("Deny LAN Discovery", "Onemogoči odkrivanje lokalnega omrežja"), + ("Write a message", "Napiši spoorčilo"), + ("Prompt", "Poziv"), + ("Please wait for confirmation of UAC...", "Počakajte za potrditev nadzora uporabniškega računa"), + ("elevated_foreground_window_tip", "Trenutno aktivno okno na oddaljenem računalniku zahteva višje pravice za upravljanje. Oddaljenega uporabnika lahko prosite, da okno minimizira, ali pa kliknite gumb za povzdig pravic v oknu za upravljanje povezave. Če se želite izogniti temu problemu, na oddaljenem računalniku RustDesk namestite."), + ("Disconnected", "Brez povezave"), + ("Other", "Drugo"), + ("Confirm before closing multiple tabs", "Zahtevajte potrditev pred zapiranjem večih zavihkov"), + ("Keyboard Settings", "Nastavitve tipkovnice"), + ("Full Access", "Poln dostop"), + ("Screen Share", "Deljenje zaslona"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahteva Ubuntu 21.04 ali novejši"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Zahtevana je novejša različica Waylanda. Posodobite vašo distribucijo ali pa uporabite X11."), + ("JumpLink", "Pogled"), + ("Please Select the screen to be shared(Operate on the peer side).", "Izberite zaslon za delitev (na oddaljeni strani)."), + ("Show RustDesk", "Prikaži RustDesk"), + ("This PC", "Ta računalnik"), + ("or", "ali"), + ("Continue with", "Nadaljuj z"), + ("Elevate", "Povzdig pravic"), + ("Zoom cursor", "Prilagodi velikost miškinega kazalca"), + ("Accept sessions via password", "Sprejmi seje z geslom"), + ("Accept sessions via click", "Sprejmi seje s potrditvijo"), + ("Accept sessions via both", "Sprejmi seje z geslom ali potrditvijo"), + ("Please wait for the remote side to accept your session request...", "Počakajte, da oddaljeni računalnik sprejme povezavo..."), + ("One-time Password", "Enkratno geslo"), + ("Use one-time password", "Uporabi enkratno geslo"), + ("One-time password length", "Dolžina enkratnega gesla"), + ("Request access to your device", "Zahtevaj dostop do svoje naprave"), + ("Hide connection management window", "Skrij okno za upravljanje povezave"), + ("hide_cm_tip", "Dovoli skrivanje samo pri sprejemanju sej z geslom"), + ("wayland_experiment_tip", "Podpora za Wayland je v preizkusni fazi. Uporabite X11, če rabite nespremljan dostop."), + ("Right click to select tabs", "Desno-kliknite za izbiro zavihkov"), + ("Skipped", "Izpuščeno"), + ("Add to Address Book", "Dodaj v adresar"), + ("Group", "Skupina"), + ("Search", "Iskanje"), + ("Closed manually by the web console", "Ročno zaprto iz spletne konzole"), + ("Local keyboard type", "Lokalna vrsta tipkovnice"), + ("Select local keyboard type", "Izberite lokalno vrsto tipkovnice"), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 5ec59c4be..0725d02e5 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Ndryshoni ID"), ("Website", "Faqe ëebi"), ("About", "Rreth"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Pa zë"), ("Audio Input", "Inputi zërit"), ("Enhancements", "Përmirësimet"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Gjithmonë lidheni me transmetues"), ("whitelist_tip", "Vetëm IP e listës së bardhë mund të më aksesoj."), ("Login", "Hyrje"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Dalje"), ("Tags", "Tage"), ("Search ID", "Kerko ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", " E përsjhtatshme në shkallë"), ("General", "Gjeneral"), ("Security", "Siguria"), - ("Account", "Llogaria"), ("Theme", "Theme"), ("Dark Theme", "Theme e errët"), ("Dark", "E errët"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Qasje e drejtpërdrejtë IP"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Apliko"), ("Disconnect all devices?", "Shkyç të gjitha pajisjet?"), ("Clear", "Pastro"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Tjetër"), ("Confirm before closing multiple tabs", "Konfirmo përpara se të mbyllësh shumë skeda"), ("Keyboard Settings", "Cilësimet e tastierës"), - ("Custom", "Personalizuar"), ("Full Access", "Qasje e plotë"), ("Screen Share", "Ndarja e ekranit"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kërkon Ubuntu 21.04 ose version më të lartë"), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Kjo është e mundur vetëm nëse aksesi bëhet nëpërmjet një fjalëkalimi të përhershëm"), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs new file mode 100644 index 000000000..3b7201bb8 --- /dev/null +++ b/src/lang/sr.rs @@ -0,0 +1,417 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Vaša radna površina"), + ("desk_tip", "Vašoj radnoj površini se može pristupiti ovim ID i lozinkom."), + ("Password", "Lozinka"), + ("Ready", "Spremno"), + ("Established", "Uspostavljeno"), + ("connecting_status", "Spajanje na RustDesk mrežu..."), + ("Enable Service", "Dozvoli servis"), + ("Start Service", "Pokreni servis"), + ("Service is running", "Servis je pokrenut"), + ("Service is not running", "Servis nije pokrenut"), + ("not_ready_status", "Nije spremno. Proverite konekciju."), + ("Control Remote Desktop", "Upravljanje udaljenom radnom površinom"), + ("Transfer File", "Prenos fajla"), + ("Connect", "Spajanje"), + ("Recent Sessions", "Poslednje sesije"), + ("Address Book", "Adresar"), + ("Confirmation", "Potvrda"), + ("TCP Tunneling", "TCP tunel"), + ("Remove", "Ukloni"), + ("Refresh random password", "Osveži slučajnu lozinku"), + ("Set your own password", "Postavi lozinku"), + ("Enable Keyboard/Mouse", "Dozvoli tastaturu/miša"), + ("Enable Clipboard", "Dozvoli clipboard"), + ("Enable File Transfer", "Dozvoli prenos fajlova"), + ("Enable TCP Tunneling", "Dozvoli TCP tunel"), + ("IP Whitelisting", "IP pouzdana lista"), + ("ID/Relay Server", "ID/Posredni server"), + ("Import Server Config", "Import server konfiguracije"), + ("Export Server Config", "Eksport server konfiguracije"), + ("Import server configuration successfully", "Import server konfiguracije uspešan"), + ("Export server configuration successfully", "Eksport server konfiguracije uspešan"), + ("Invalid server configuration", "Pogrešna konfiguracija servera"), + ("Clipboard is empty", "Clipboard je prazan"), + ("Stop service", "Stopiraj servis"), + ("Change ID", "Promeni ID"), + ("Website", "Web sajt"), + ("About", "O programu"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Utišaj"), + ("Audio Input", "Audio ulaz"), + ("Enhancements", "Proširenja"), + ("Hardware Codec", "Hardverski kodek"), + ("Adaptive Bitrate", "Prilagodljiva gustina podataka"), + ("ID Server", "ID server"), + ("Relay Server", "Posredni server"), + ("API Server", "API server"), + ("invalid_http", "mora početi sa http:// ili https://"), + ("Invalid IP", "Nevažeća IP"), + ("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."), + ("Invalid format", "Pogrešan format"), + ("server_not_support", "Server još uvek ne podržava"), + ("Not available", "Nije dostupno"), + ("Too frequent", "Previše često"), + ("Cancel", "Otkaži"), + ("Skip", "Preskoči"), + ("Close", "Zatvori"), + ("Retry", "Ponovi"), + ("OK", "Ok"), + ("Password Required", "Potrebna lozinka"), + ("Please enter your password", "Molimo unesite svoju lozinku"), + ("Remember password", "Zapamti lozinku"), + ("Wrong Password", "Pogrešna lozinka"), + ("Do you want to enter again?", "Želite li da unesete ponovo?"), + ("Connection Error", "Greška u konekciji"), + ("Error", "Greška"), + ("Reset by the peer", "Prekinuto sa druge strane"), + ("Connecting...", "Povezivanje..."), + ("Connection in progress. Please wait.", "Povezivanje u toku. Molimo sačekajte."), + ("Please try 1 minute later", "Pokušajte minut kasnije"), + ("Login Error", "Greška u prijavljivanju"), + ("Successful", "Uspešno"), + ("Connected, waiting for image...", "Spojeno, sačekajte sliku..."), + ("Name", "Ime"), + ("Type", "Tip"), + ("Modified", "Izmenjeno"), + ("Size", "Veličina"), + ("Show Hidden Files", "Prikaži skrivene datoteke"), + ("Receive", "Prijem"), + ("Send", "Slanje"), + ("Refresh File", "Osveži datoteku"), + ("Local", "Lokalno"), + ("Remote", "Udaljeno"), + ("Remote Computer", "Udaljeni računar"), + ("Local Computer", "Lokalni računar"), + ("Confirm Delete", "Potvrdite brisanje"), + ("Delete", "Brisanje"), + ("Properties", "Osobine"), + ("Multi Select", "Višestruko selektovanje"), + ("Select All", "Selektuj sve"), + ("Unselect All", "Deselektuj sve"), + ("Empty Directory", "Prazan direktorijum"), + ("Not an empty directory", "Nije prazan direktorijum"), + ("Are you sure you want to delete this file?", "Da li ste sigurni da želite da obrišete ovu datoteku?"), + ("Are you sure you want to delete this empty directory?", "Da li ste sigurni da želite da obrišete ovaj prazan direktorijum?"), + ("Are you sure you want to delete the file of this directory?", "Da li ste sigurni da želite da obrišete datoteku ovog direktorijuma?"), + ("Do this for all conflicts", "Uradi ovo za sve konflikte"), + ("This is irreversible!", "Ovo je nepovratno"), + ("Deleting", "Brisanje"), + ("files", "datoteke"), + ("Waiting", "Čekanje"), + ("Finished", "Završeno"), + ("Speed", "Brzina"), + ("Custom Image Quality", "Korisnički kvalitet slike"), + ("Privacy mode", "Mod privatnosti"), + ("Block user input", "Blokiraj korisnikov unos"), + ("Unblock user input", "Odblokiraj korisnikov unos"), + ("Adjust Window", "Podesi prozor"), + ("Original", "Original"), + ("Shrink", "Skupi"), + ("Stretch", "Raširi"), + ("Scrollbar", "Skrol linija"), + ("ScrollAuto", "Auto skrol"), + ("Good image quality", "Dobar kvalitet slike"), + ("Balanced", "Balansirano"), + ("Optimize reaction time", "Optimizuj vreme reakcije"), + ("Custom", "Korisnički"), + ("Show remote cursor", "Prikaži udaljeni kursor"), + ("Show quality monitor", "Prikaži monitor kvaliteta"), + ("Disable clipboard", "Zabrani clipboard"), + ("Lock after session end", "Zaključaj po završetku sesije"), + ("Insert", "Umetni"), + ("Insert Lock", "Zaključaj umetanje"), + ("Refresh", "Osveži"), + ("ID does not exist", "ID ne postoji"), + ("Failed to connect to rendezvous server", "Greška u spajanju na server za povezivanje"), + ("Please try later", "Molimo pokušajte kasnije"), + ("Remote desktop is offline", "Udaljeni ekran je isključen"), + ("Key mismatch", "Pogrešan ključ"), + ("Timeout", "Isteklo vreme"), + ("Failed to connect to relay server", "Greška u spajanju na posredni server"), + ("Failed to connect via rendezvous server", "Greška u spajanju preko servera za povezivanje"), + ("Failed to connect via relay server", "Greška u spajanju preko posrednog servera"), + ("Failed to make direct connection to remote desktop", "Greška u direktnom spajanju na udaljenu radnu površinu"), + ("Set Password", "Postavi lozinku"), + ("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."), + ("Installing ...", "Instaliranje..."), + ("Install", "Instaliraj"), + ("Installation", "Instalacija"), + ("Installation Path", "Putanja za instalaciju"), + ("Create start menu shortcuts", "Kreiraj prečice u meniju"), + ("Create desktop icon", "Kreiraj ikonicu na radnoj površini"), + ("agreement_tip", "Pokretanjem instalacije prihvatate ugovor o licenciranju."), + ("Accept and Install", "Prihvati i instaliraj"), + ("End-user license agreement", "Ugovor sa krajnjim korisnikom"), + ("Generating ...", "Generisanje..."), + ("Your installation is lower version.", "Vaša instalacija je niže verzije"), + ("not_close_tcp_tip", "Ne zatvarajte ovaj prozor dok koristite tunel"), + ("Listening ...", "Na slušanju..."), + ("Remote Host", "Adresa udaljenog uređaja"), + ("Remote Port", "Udaljeni port"), + ("Action", "Akcija"), + ("Add", "Dodaj"), + ("Local Port", "Lokalni port"), + ("Local Address", "Lokalna adresa"), + ("Change Local Port", "Promeni lokalni port"), + ("setup_server_tip", "Za brže spajanje, molimo da koristite svoj server"), + ("Too short, at least 6 characters.", "Prekratko, najmanje 6 znakova."), + ("The confirmation is not identical.", "Potvrda nije identična"), + ("Permissions", "Dozvole"), + ("Accept", "Prihvati"), + ("Dismiss", "Odbaci"), + ("Disconnect", "Raskini konekciju"), + ("Allow using keyboard and mouse", "Dozvoli korišćenje tastature i miša"), + ("Allow using clipboard", "Dozvoli korišćenje clipboard-a"), + ("Allow hearing sound", "Dozvoli da se čuje zvuk"), + ("Allow file copy and paste", "Dozvoli kopiranje i lepljenje fajlova"), + ("Connected", "Spojeno"), + ("Direct and encrypted connection", "Direktna i kriptovana konekcija"), + ("Relayed and encrypted connection", "Posredna i kriptovana konekcija"), + ("Direct and unencrypted connection", "Direktna i nekriptovana konekcija"), + ("Relayed and unencrypted connection", "Posredna i nekriptovana konekcija"), + ("Enter Remote ID", "Unesite ID udaljenog uređaja"), + ("Enter your password", "Unesite svoju lozinku"), + ("Logging in...", "Prijava..."), + ("Enable RDP session sharing", "Dozvoli deljenje RDP sesije"), + ("Auto Login", "Auto prijavljivanje (Važeće samo ako ste postavili \"Lock after session end\")"), + ("Enable Direct IP Access", "Dozvoli direktan pristup preko IP"), + ("Rename", "Preimenuj"), + ("Space", "Prazno"), + ("Create Desktop Shortcut", "Kreiraj prečicu na radnoj površini"), + ("Change Path", "Promeni putanju"), + ("Create Folder", "Kreiraj direktorijum"), + ("Please enter the folder name", "Unesite ime direktorijuma"), + ("Fix it", "Popravi ga"), + ("Warning", "Upozorenje"), + ("Login screen using Wayland is not supported", "Ekran za prijavu koji koristi Wayland nije podržan"), + ("Reboot required", "Potreban je restart"), + ("Unsupported display server ", "Nepodržan server za prikaz"), + ("x11 expected", "x11 očekivan"), + ("Port", "Port"), + ("Settings", "Postavke"), + ("Username", "Korisničko ime"), + ("Invalid port", "Pogrešan port"), + ("Closed manually by the peer", "Klijent ručno raskinuo konekciju"), + ("Enable remote configuration modification", "Dozvoli modifikaciju udaljene konfiguracije"), + ("Run without install", "Pokreni bez instalacije"), + ("Always connected via relay", "Uvek spojne preko posrednika"), + ("Always connect via relay", "Uvek se spoj preko posrednika"), + ("whitelist_tip", "Samo dozvoljene IP mi mogu pristupiti"), + ("Login", "Prijava"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), + ("Logout", "Odjava"), + ("Tags", "Oznake"), + ("Search ID", "Traži ID"), + ("Current Wayland display server is not supported", "Tekući Wazland server za prikaz nije podržan"), + ("whitelist_sep", "Odvojeno zarezima, tačka zarezima, praznim mestima ili novim redovima"), + ("Add ID", "Dodaj ID"), + ("Add Tag", "Dodaj oznaku"), + ("Unselect all tags", "Odselektuj sve oznake"), + ("Network error", "Greška na mreži"), + ("Username missed", "Korisničko ime promašeno"), + ("Password missed", "Lozinka promašena"), + ("Wrong credentials", "Pogrešno korisničko ime ili lozinka"), + ("Edit Tag", "Izmeni oznaku"), + ("Unremember Password", "Zaboravi lozinku"), + ("Favorites", "Favoriti"), + ("Add to Favorites", "Dodaj u favorite"), + ("Remove from Favorites", "Izbaci iz favorita"), + ("Empty", "Prazno"), + ("Invalid folder name", "Pogrešno ime direktorijuma"), + ("Socks5 Proxy", "Socks5 proksi"), + ("Hostname", "Ime uređaja"), + ("Discovered", "Otkriveno"), + ("install_daemon_tip", "Za pokretanje pri startu sistema, treba da instalirate sistemski servis."), + ("Remote ID", "Udaljeni ID"), + ("Paste", "Nalepi"), + ("Paste here?", "Nalepi ovde?"), + ("Are you sure to close the connection?", "Da li ste sigurni da želite da zatvorite konekciju?"), + ("Download new version", "Preuzmi novu verziju"), + ("Touch mode", "Mod na dodir"), + ("Mouse mode", "Miš mod"), + ("One-Finger Tap", "Pritisak jednim prstom"), + ("Left Mouse", "Levi miš"), + ("One-Long Tap", "Dugi pritisak"), + ("Two-Finger Tap", "Pritisak sa dva prsta"), + ("Right Mouse", "Desni miš"), + ("One-Finger Move", "Pomeranje jednim prstom"), + ("Double Tap & Move", "Dupli pritisak i pomeranje"), + ("Mouse Drag", "Prevlačenje mišem"), + ("Three-Finger vertically", "Sa tri prsta vertikalno"), + ("Mouse Wheel", "Točkić miša"), + ("Two-Finger Move", "Pomeranje sa dva prsta"), + ("Canvas Move", "Pomeranje pozadine"), + ("Pinch to Zoom", "Stisnite za zumiranje"), + ("Canvas Zoom", "Zumiranje pozadine"), + ("Reset canvas", "Resetuj pozadinu"), + ("No permission of file transfer", "Nemate pravo prenosa datoteka"), + ("Note", "Primedba"), + ("Connection", "Konekcija"), + ("Share Screen", "Podeli ekran"), + ("CLOSE", "ZATVORI"), + ("OPEN", "OTVORI"), + ("Chat", "Dopisivanje"), + ("Total", "Ukupno"), + ("items", "stavki"), + ("Selected", "Izabrano"), + ("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?"), + ("android_input_permission_tip1", "Da bi daljinski uređaj kontrolisao vaš Android uređaj preko miša ili na dodir, treba da dozvolite RustDesk-u da koristi \"Accessibility\" servis."), + ("android_input_permission_tip2", "Molimo pređite na sledeću stranicu sistemskih podešavanja, pronađite i unesite [Installed Services], uključite [RustDesk Input] servis."), + ("android_new_connection_tip", "Primljen je novi zahtev za upravljanje, koji želi da upravlja ovim vašim uređajem."), + ("android_service_will_start_tip", "Uključenje \"Screen Capture\" automatski će pokrenuti servis, dozvoljavajući drugim uređajima da zahtevaju spajanje na vaš uređaj."), + ("android_stop_service_tip", "Zatvaranje servisa automatski će zatvoriti sve uspostavljene konekcije."), + ("android_version_audio_tip", "Tekuća Android verzija ne podržava audio snimanje, molimo nadogradite na Android 10 ili veći."), + ("android_start_service_tip", "Kliknite [Start Service] ili OPEN [Screen Capture] dozvolu da pokrenete servis deljenja ekrana."), + ("Account", "Nalog"), + ("Overwrite", "Prepiši preko"), + ("This file exists, skip or overwrite this file?", "Ova datoteka postoji, preskoči ili prepiši preko?"), + ("Quit", "Izlaz"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Pomoć"), + ("Failed", "Greška"), + ("Succeeded", "Uspešno"), + ("Someone turns on privacy mode, exit", "Neko je uključio mod privatnosti, izlaz."), + ("Unsupported", "Nepodržano"), + ("Peer denied", "Klijent zabranjen"), + ("Please install plugins", "Molimo instalirajte dodatke"), + ("Peer exit", "Klijent izašao"), + ("Failed to turn off", "Greška kod isključenja"), + ("Turned off", "Isključeno"), + ("In privacy mode", "U modu privatnosti"), + ("Out privacy mode", "Van moda privatnosti"), + ("Language", "Jezik"), + ("Keep RustDesk background service", "Zadrži RustDesk kao pozadinski servis"), + ("Ignore Battery Optimizations", "Zanemari optimizacije baterije"), + ("android_open_battery_optimizations_tip", "Ako želite da onemogućite ovu funkciju, molimo idite na sledeću stranicu za podešavanje RustDesk aplikacije, pronađite i uđite u [Battery], isključite [Unrestricted]"), + ("Connection not allowed", "Konekcija nije dozvoljena"), + ("Legacy mode", "Zastareli mod"), + ("Map mode", "Mod mapiranja"), + ("Translate mode", "Mod prevođenja"), + ("Use permanent password", "Koristi trajnu lozinku"), + ("Use both passwords", "Koristi obe lozinke"), + ("Set permanent password", "Postavi trajnu lozinku"), + ("Enable Remote Restart", "Omogući daljinsko restartovanje"), + ("Allow remote restart", "Dozvoli daljinsko restartovanje"), + ("Restart Remote Device", "Restartuj daljinski uređaj"), + ("Are you sure you want to restart", "Da li ste sigurni da želite restart"), + ("Restarting Remote Device", "Restartovanje daljinskog uređaja"), + ("remote_restarting_tip", "Udaljeni uređaj se restartuje, molimo zatvorite ovu poruku i ponovo se kasnije povežite trajnom šifrom"), + ("Copied", "Kopirano"), + ("Exit Fullscreen", "Napusti mod celog ekrana"), + ("Fullscreen", "Mod celog ekrana"), + ("Mobile Actions", "Mobilne akcije"), + ("Select Monitor", "Izbor monitora"), + ("Control Actions", "Upravljačke akcije"), + ("Display Settings", "Postavke prikaza"), + ("Ratio", "Odnos"), + ("Image Quality", "Kvalitet slike"), + ("Scroll Style", "Stil skrolovanja"), + ("Show Menubar", "Prikaži meni"), + ("Hide Menubar", "Sakrij meni"), + ("Direct Connection", "Direktna konekcija"), + ("Relay Connection", "Posredna konekcija"), + ("Secure Connection", "Bezbedna konekcija"), + ("Insecure Connection", "Nebezbedna konekcija"), + ("Scale original", "Skaliraj original"), + ("Scale adaptive", "Adaptivno skaliranje"), + ("General", "Uopšteno"), + ("Security", "Bezbednost"), + ("Theme", "Tema"), + ("Dark Theme", "Tamna tema"), + ("Dark", "Tamno"), + ("Light", "Svetlo"), + ("Follow System", "Prema sistemu"), + ("Enable hardware codec", "Omogući hardverski kodek"), + ("Unlock Security Settings", "Otključaj postavke bezbednosti"), + ("Enable Audio", "Dozvoli zvuk"), + ("Unlock Network Settings", "Otključaj postavke mreže"), + ("Server", "Server"), + ("Direct IP Access", "Direktan IP pristup"), + ("Proxy", "Proksi"), + ("Apply", "Primeni"), + ("Disconnect all devices?", "Otkači sve uređaju?"), + ("Clear", "Obriši"), + ("Audio Input Device", "Uređaj za ulaz zvuka"), + ("Deny remote access", "Zabrani daljinski pristup"), + ("Use IP Whitelisting", "Koristi listu pouzdanih IP"), + ("Network", "Mreža"), + ("Enable RDP", "Dozvoli RDP"), + ("Pin menubar", "Zakači meni"), + ("Unpin menubar", "Otkači meni"), + ("Recording", "Snimanje"), + ("Directory", "Direktorijum"), + ("Automatically record incoming sessions", "Automatski snimaj dolazne sesije"), + ("Change", "Promeni"), + ("Start session recording", "Započni snimanje sesije"), + ("Stop session recording", "Zaustavi snimanje sesije"), + ("Enable Recording Session", "Omogući snimanje sesije"), + ("Allow recording session", "Dozvoli snimanje sesije"), + ("Enable LAN Discovery", "Omogući LAN otkrivanje"), + ("Deny LAN Discovery", "Zabrani LAN otkrivanje"), + ("Write a message", "Napiši poruku"), + ("Prompt", "Prompt"), + ("Please wait for confirmation of UAC...", "Molimo sačekajte UAC potvrdu..."), + ("elevated_foreground_window_tip", "Tekući prozor udaljene radne površine zahteva veću privilegiju za rad, tako da trenutno nije moguće koristiti miša i tastaturu. Možete zahtevati od udaljenog korisnika da minimizira aktivni prozor, ili kliknuti na taster za podizanje privilegija u prozoru za rad sa konekcijom. Da biste prevazišli ovaj problem, preporučljivo je da instalirate softver na udaljeni uređaj."), + ("Disconnected", "Odspojeno"), + ("Other", "Ostalo"), + ("Confirm before closing multiple tabs", "Potvrda pre zatvaranja više kartica"), + ("Keyboard Settings", "Postavke tastature"), + ("Full Access", "Pun pristup"), + ("Screen Share", "Deljenje ekrana"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahteva Ubuntu 21.04 ili veću verziju"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland zahteva veću verziju Linux distribucije. Molimo pokušajte X11 ili promenite OS."), + ("JumpLink", "Vidi"), + ("Please Select the screen to be shared(Operate on the peer side).", "Molimo izaberite ekran koji će biti podeljen (Za rad na klijent strani)"), + ("Show RustDesk", "Prikazi RustDesk"), + ("This PC", "Ovaj PC"), + ("or", "ili"), + ("Continue with", "Nastavi sa"), + ("Elevate", "Izdigni"), + ("Zoom cursor", "Zumiraj kursor"), + ("Accept sessions via password", "Prihvati sesije preko lozinke"), + ("Accept sessions via click", "Prihvati sesije preko klika"), + ("Accept sessions via both", "Prihvati sesije preko oboje"), + ("Please wait for the remote side to accept your session request...", "Molimo sačekajte da udaljena strana prihvati vaš zahtev za sesijom..."), + ("One-time Password", "Jednokratna lozinka"), + ("Use one-time password", "Koristi jednokratnu lozinku"), + ("One-time password length", "Dužina jednokratne lozinke"), + ("Request access to your device", "Zahtev za pristup vašem uređaju"), + ("Hide connection management window", "Sakrij prozor za uređivanje konekcije"), + ("hide_cm_tip", "Skrivanje dozvoljeno samo prihvatanjem sesije preko lozinke i korišćenjem trajne lozinke"), + ("wayland_experiment_tip", "Wayland eksperiment savet"), + ("Right click to select tabs", "Desni klik za izbor kartica"), + ("Skipped", ""), + ("Add to Address Book", "Dodaj u adresar"), + ("Group", "Grupa"), + ("Search", "Pretraga"), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 1feb5d55e..eeeec80cc 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Byt ID"), ("Website", "Hemsida"), ("About", "Om"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Tyst"), ("Audio Input", "Ljud input"), ("Enhancements", "Förbättringar"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Anslut alltid via relay"), ("whitelist_tip", "Bara vitlistade IPs kan koppla upp till mig"), ("Login", "Logga in"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Logga ut"), ("Tags", "Taggar"), ("Search ID", "Sök ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skala adaptivt"), ("General", "Generellt"), ("Security", "Säkerhet"), - ("Account", "Konto"), ("Theme", "Tema"), ("Dark Theme", "Mörkt tema"), ("Dark", "Mörk"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direkt IP åtkomst"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Tillämpa"), ("Disconnect all devices?", "Koppla ifrån alla enheter?"), ("Clear", "Töm"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Övrigt"), ("Confirm before closing multiple tabs", "Bekräfta innan du stänger flera flikar"), ("Keyboard Settings", "Tangentbordsinställningar"), - ("Custom", "Anpassat"), ("Full Access", "Full tillgång"), ("Screen Share", "Skärmdelning"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kräver Ubuntu 21.04 eller högre."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Tillåt att gömma endast om accepterande sessioner med lösenord och permanenta lösenord"), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 6993cb43c..d3be7ba17 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", ""), ("Website", ""), ("About", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", ""), ("Audio Input", ""), ("Enhancements", ""), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", ""), ("whitelist_tip", ""), ("Login", ""), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", ""), ("Tags", ""), ("Search ID", ""), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", ""), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", ""), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs new file mode 100644 index 000000000..a4d0a033d --- /dev/null +++ b/src/lang/th.rs @@ -0,0 +1,417 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "สถานะ"), + ("Your Desktop", "หน้าจอของคุณ"), + ("desk_tip", "คุณสามารถเข้าถึงเดสก์ท็อปของคุณได้ด้วย ID และรหัสผ่านต่อไปนี้"), + ("Password", "รหัสผ่าน"), + ("Ready", "พร้อม"), + ("Established", "เชื่อมต่อแล้ว"), + ("connecting_status", "กำลังเชื่อมต่อไปยังเครือข่าย RustDesk..."), + ("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", "IP ไวท์ลิสต์"), + ("ID/Relay Server", "เซิร์ฟเวอร์ ID/Relay"), + ("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"), + ("Website", "เว็บไซต์"), + ("About", "เกี่ยวกับ"), + ("Slogan_tip", "ทำด้วยใจ ในโลกใบนี้ที่ยุ่งเหยิง!"), + ("Privacy Statement", "คำแถลงเกี่ยวกับความเป็นส่วนตัว"), + ("Mute", "ปิดเสียง"), + ("Audio Input", "ออดิโออินพุท"), + ("Enhancements", "การปรับปรุง"), + ("Hardware Codec", "ฮาร์ดแวร์ codec"), + ("Adaptive Bitrate", "บิทเรทผันแปร"), + ("ID Server", "เซิร์ฟเวอร์ ID"), + ("Relay Server", "เซิร์ฟเวอร์ Relay"), + ("API Server", "เซิร์ฟเวอร์ API"), + ("invalid_http", "ต้องขึ้นต้นด้วย http:// หรือ https:// เท่านั้น"), + ("Invalid IP", "IP ไม่ถูกต้อง"), + ("id_change_tip", "อนุญาตเฉพาะตัวอักษร a-z A-Z 0-9 และ _ (ขีดล่าง) เท่านั้น โดยตัวอักษรขึ้นต้นจะต้องเป็น a-z หรือไม่ก็ A-Z และมีความยาวระหว่าง 6 ถึง 16 ตัวอักษร"), + ("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", "แทรก"), + ("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", "การเชื่อมต่อตรงไปยังเดสก์ท็อปปลายทางล้มเหลว"), + ("Set Password", "ตั้งรหัสผ่าน"), + ("OS Password", "รหัสผ่านระบบปฏิบัติการ"), + ("install_tip", "เนื่องด้วยข้อจำกัดของการใช้งาน UAC ทำให้ RustDesk ไม่สามารถทำงานได้ปกติในฝั่งปลายทางในบางครั้ง เพื่อหลีกเลี่ยงข้อจำกัดของ UAC กรุณากดปุ่มด้านล่างเพื่อติดตั้ง RustDesk ไปยังระบบของคุณ"), + ("Click to upgrade", "คลิกเพื่ออัปเกรด"), + ("Click to download", "คลิกเพื่อดาวน์โหลด"), + ("Click to update", "คลิกเพื่ออัปเดต"), + ("Configure", "ปรับแต่งค่า"), + ("config_acc", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่ RustDesk"), + ("config_screen", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การบันทึกภาพหน้าจอ\" ให้แก่ RustDesk"), + ("Installing ...", "กำลังติดตั้ง ..."), + ("Install", "ติดตั้ง"), + ("Installation", "การติดตั้ง"), + ("Installation Path", "ตำแหน่งที่ติดตั้ง"), + ("Create start menu shortcuts", "สร้างทางลัดไปยัง Start Menu"), + ("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", "การดำเนินการ"), + ("Add", "เพิ่ม"), + ("Local Port", "พอร์ทต้นทาง"), + ("Local Address", "ที่อยู่ต้นทาง"), + ("Change Local Port", "เปลี่ยนพอร์ทต้นทาง"), + ("setup_server_tip", "เพื่อการเชื่อมต่อที่เร็วขึ้น กรุณาเซ็ตอัปเซิร์ฟเวอร์ของคุณเอง"), + ("Too short, at least 6 characters.", "สั้นเกินไป ต้องไม่ต่ำกว่า 6 ตัวอักษร"), + ("The confirmation is not identical.", "การยืนยันข้อมูลไม่ถูกต้อง"), + ("Permissions", "สิทธิ์การใช้งาน"), + ("Accept", "ยอมรับ"), + ("Dismiss", "ปิด"), + ("Disconnect", "ยกเลิกการเชื่อมต่อ"), + ("Allow using keyboard and mouse", "อนุญาตให้ใช้งานคีย์บอร์ดและเมาส์"), + ("Allow using clipboard", "อนุญาตให้ใช้คลิปบอร์ด"), + ("Allow hearing sound", "อนุญาตให้ได้ยินเสียง"), + ("Allow file copy and paste", "อนุญาตให้มีการคัดลอกและวางไฟล์"), + ("Connected", "เชื่อมต่อแล้ว"), + ("Direct and encrypted connection", "การเชื่อมต่อตรงที่มีการเข้ารหัส"), + ("Relayed and encrypted connection", "การเชื่อมต่อแบบรีเลย์ที่มีการเข้ารหัส"), + ("Direct and unencrypted connection", "การเชื่อมต่อตรงที่ไม่มีการเข้ารหัส"), + ("Relayed and unencrypted connection", "การเชื่อมต่อแบบรีเลย์ที่ไม่มีการเข้ารหัส"), + ("Enter Remote ID", "กรอก ID ปลายทาง"), + ("Enter your password", "กรอกรหัสผ่าน"), + ("Logging in...", "กำลังเข้าสู่ระบบ..."), + ("Enable RDP session sharing", "เปิดการใช้งานการแชร์เซสชัน RDP"), + ("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", "ใช้งานโดยไม่ต้องติดตั้ง"), + ("Always connected via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), + ("Always connect via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), + ("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"), + ("Login", "เข้าสู่ระบบ"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), + ("Logout", "ออกจากระบบ"), + ("Tags", "แท็ก"), + ("Search ID", "ค้นหา ID"), + ("Current Wayland display server is not supported", "เซิร์ฟเวอร์การแสดงผล Wayland ปัจจุบันไม่รองรับ"), + ("whitelist_sep", "คั่นโดยเครื่องหมาย comma semicolon เว้นวรรค หรือ ขึ้นบรรทัดใหม่"), + ("Add ID", "เพิ่ม ID"), + ("Add Tag", "เพิ่มแท็ก"), + ("Unselect all tags", "ยกเลิกการเลือกแท็กทั้งหมด"), + ("Network error", "ข้อผิดพลาดของเครือข่าย"), + ("Username missed", "ไม่พบข้อมูลผู้ใช้งาน"), + ("Password missed", "ไม่พบรหัสผ่าน"), + ("Wrong credentials", "ข้อมูลสำหรับเข้าสู่ระบบไม่ถูกต้อง"), + ("Edit Tag", "แก้ไขแท็ก"), + ("Unremember Password", "ยกเลิกการจดจำรหัสผ่าน"), + ("Favorites", "รายการโปรด"), + ("Add to Favorites", "เพิ่มไปยังรายการโปรด"), + ("Remove from Favorites", "ลบออกจากรายการโปรด"), + ("Empty", "ว่างเปล่า"), + ("Invalid folder name", "ชื่อโฟลเดอร์ไม่ถูกต้อง"), + ("Socks5 Proxy", "พรอกซี Socks5"), + ("Hostname", "ชื่อโฮสต์"), + ("Discovered", "ค้นพบ"), + ("install_daemon_tip", "หากต้องการใช้งานขณะระบบเริ่มต้น คุณจำเป็นจะต้องติดตั้งเซอร์วิส"), + ("Remote ID", "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", "แชร์หน้าจอ"), + ("CLOSE", "ปิด"), + ("OPEN", "เปิด"), + ("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", "ในการที่จะอนุญาตให้เครื่องปลายทางควบคุมอุปกรณ์แอนดรอยด์ของคุณโดยใช้เมาส์หรือการสัมผัส คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่เซอร์วิสของ RustDesk"), + ("android_input_permission_tip2", "กรุณาไปยังหน้าตั้งค่าถัดไป ค้นหาและเข้าไปยัง [เซอร์วิสที่ถูกติดตั้ง] และเปิดการใช้งานเซอร์วิส [อินพุท RustDesk]"), + ("android_new_connection_tip", "ได้รับคำขอควบคุมใหม่ที่ต้องการควบคุมอุปกรณ์ของคุณ"), + ("android_service_will_start_tip", "การเปิดการใช้งาน \"การบันทึกหน้าจอ\" จะเป็นการเริ่มต้นการทำงานของเซอร์วิสโดยอัตโนมัติ ที่จะอนุญาตให้อุปกรณ์อื่นๆ ส่งคำขอเข้าถึงมายังอุปกรณ์ของคุณได้"), + ("android_stop_service_tip", "การปิดการใช้งานเซอร์วิสจะปิดการเชื่อมต่อทั้งหมดโดยอัตโนมัติ"), + ("android_version_audio_tip", "เวอร์ชั่นแอนดรอยด์ปัจจุบันของคุณไม่รองรับการบันทึกข้อมูลเสียง กรุณาอัปเกรดเป็นแอนดรอยด์เวอร์ชั่น 10 หรือสูงกว่า"), + ("android_start_service_tip", "แตะ [เริ่มต้นใช้งานเซอร์วิส] หรือเปิดสิทธิ์ [การบันทึกหน้าจอ] เพื่อเริ่มเซอร์วิสการแชร์หน้าจอ"), + ("Account", "บัญชี"), + ("Overwrite", "เขียนทับ"), + ("This file exists, skip or overwrite this file?", "พบไฟล์ที่มีอยู่แล้ว ต้องการเขียนทับหรือไม่?"), + ("Quit", "ออก"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "ช่วยเหลือ"), + ("Failed", "ล้มเหลว"), + ("Succeeded", "สำเร็จ"), + ("Someone turns on privacy mode, exit", "มีใครบางคนเปิดใช้งานโหมดความเป็นส่วนตัว กำลังออก"), + ("Unsupported", "ไม่รองรับ"), + ("Peer denied", "ถูกปฏิเสธโดยอีกฝั่ง"), + ("Please install plugins", "กรุณาติดตั้งปลั๊กอิน"), + ("Peer exit", "อีกฝั่งออก"), + ("Failed to turn off", "การปิดล้มเหลว"), + ("Turned off", "ปิด"), + ("In privacy mode", "อยู่ในโหมดความเป็นส่วนตัว"), + ("Out privacy mode", "อยู่นอกโหมดความเป็นส่วนตัว"), + ("Language", "ภาษา"), + ("Keep RustDesk background service", "คงสถานะการทำงานเบื้องหลังของเซอร์วิส RustDesk"), + ("Ignore Battery Optimizations", "เพิกเฉยการตั้งค่าการใช้งาน Battery Optimization"), + ("android_open_battery_optimizations_tip", "หากคุณต้องการปิดการใช้งานฟีเจอร์นี้ กรุณาไปยังหน้าตั้งค่าในแอปพลิเคชัน RustDesk ค้นหาหัวข้อ [Battery] และยกเลิกการเลือกรายการ [Unrestricted]"), + ("Connection not allowed", "การเชื่อมต่อไม่อนุญาต"), + ("Legacy mode", ""), + ("Map mode", ""), + ("Translate mode", ""), + ("Use permanent password", "ใช้รหัสผ่านถาวร"), + ("Use both passwords", "ใช้รหัสผ่านทั้งสองแบบ"), + ("Set permanent password", "ตั้งค่ารหัสผ่านถาวร"), + ("Enable Remote Restart", "เปิดการใช้งานการรีสตาร์ทระบบทางไกล"), + ("Allow 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 Menubar", "แสดงแถบเมนู"), + ("Hide Menubar", "ซ่อนแถบเมนู"), + ("Direct Connection", "การเชื่อมต่อตรง"), + ("Relay Connection", "การเชื่อมต่อแบบรีเลย์"), + ("Secure Connection", "การเชื่อมต่อที่ปลอดภัย"), + ("Insecure Connection", "การเชื่อมต่อที่ไม่ปลอดภัย"), + ("Scale original", "ขนาดเดิม"), + ("Scale adaptive", "ขนาดยืดหยุ่น"), + ("General", "ทั่วไป"), + ("Security", "ความปลอดภัย"), + ("Theme", "ธีม"), + ("Dark Theme", "ธีมมืด"), + ("Dark", "มืด"), + ("Light", "สว่าง"), + ("Follow System", "ตามระบบ"), + ("Enable hardware codec", "เปิดการใช้งานฮาร์ดแวร์ codec"), + ("Unlock Security Settings", "ปลดล็อคการตั้งค่าความปลอดภัย"), + ("Enable Audio", "เปิดการใช้งานเสียง"), + ("Unlock Network Settings", "ปลดล็อคการตั้งค่าเครือข่าย"), + ("Server", "เซิร์ฟเวอร์"), + ("Direct IP Access", "การเข้าถึง IP ตรง"), + ("Proxy", "พรอกซี"), + ("Apply", "นำไปใช้"), + ("Disconnect all devices?", "ยกเลิกการเชื่อมต่ออุปกรณ์ทั้งหมด?"), + ("Clear", "ล้างข้อมูล"), + ("Audio Input Device", "อุปกรณ์รับอินพุทข้อมูลเสียง"), + ("Deny remote access", "ปฏิเสธการเชื่อมต่อ"), + ("Use IP Whitelisting", "ใช้งาน IP ไวท์ลิสต์"), + ("Network", "เครือข่าย"), + ("Enable RDP", "เปิดการใช้งาน RDP"), + ("Pin menubar", "ปักหมุดแถบเมนู"), + ("Unpin menubar", "ยกเลิกการปักหมุดแถบเมนู"), + ("Recording", "การบันทึก"), + ("Directory", "ไดเรกทอรี่"), + ("Automatically record incoming sessions", "บันทึกเซสชันขาเข้าโดยอัตโนมัติ"), + ("Change", "เปลี่ยน"), + ("Start session recording", "เริ่มต้นการบันทึกเซสชัน"), + ("Stop session recording", "หยุดการบันทึกเซสซัน"), + ("Enable Recording Session", "เปิดใช้งานการบันทึกเซสชัน"), + ("Allow 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 ต้องการลินุกซ์เวอร์ชันที่สูงกว่านี้ กรุณาเปลี่ยนไปใช้เดสก์ท็อป X11 หรือเปลี่ยนระบบปฏิบัติการของคุณ"), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "กรุณาเลือกหน้าจอที่ต้องการแชร์ (ใช้งานในอีกฝั่งของการเชื่อมต่อ)"), + ("Show RustDesk", "แสดง RustDesk"), + ("This PC", ""), + ("or", "หรือ"), + ("Continue with", "ทำต่อด้วย"), + ("Elevate", "ยกระดับ"), + ("Zoom cursor", "ขยายเคอร์เซอร์"), + ("Accept sessions via password", "ยอมรับการเชื่อมต่อด้วยรหัสผ่าน"), + ("Accept sessions via click", "ยอมรับการเชื่อมต่อด้วยการคลิก"), + ("Accept sessions via both", "ยอมรับการเชื่อมต่อด้วยทั้งสองวิธิ"), + ("Please wait for the remote side to accept your session request...", "กรุณารอให้อีกฝั่งยอมรับการเชื่อมต่อของคุณ..."), + ("One-time Password", "รหัสผ่านครั้งเดียว"), + ("Use one-time password", "ใช้รหัสผ่านครั้งเดียว"), + ("One-time password length", "ความยาวรหัสผ่านครั้งเดียว"), + ("Request access to your device", "คำขอการเข้าถึงอุปกรณ์ของคุณ"), + ("Hide connection management window", "ซ่อนหน้าต่างการจัดการการเชื่อมต่อ"), + ("hide_cm_tip", "อนุญาตการซ่อนก็ต่อเมื่อยอมรับการเชื่อมต่อด้วยรหัสผ่าน และต้องเป็นรหัสผ่านถาวรเท่านั้น"), + ("wayland_experiment_tip", "การสนับสนุน Wayland ยังอยู่ในขั้นตอนการทดลอง กรุณาใช้ X11 หากคุณต้องการใช้งานการเข้าถึงแบบไม่มีผู้ดูแล"), + ("Right click to select tabs", "คลิกขวาเพื่อเลือกแท็บ"), + ("Skipped", "ข้าม"), + ("Add to Address Book", "เพิ่มไปยังสมุดรายชื่อ"), + ("Group", "กลุ่ม"), + ("Search", "ค้นหา"), + ("Closed manually by the web console", "ถูกปิดโดยเว็บคอนโซล"), + ("Local keyboard type", "ประเภทคีย์บอร์ด"), + ("Select local keyboard type", "เลือกประเภทคีย์บอร์ด"), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 7b66af60e..2d0fc8c59 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID Değiştir"), ("Website", "Website"), ("About", "Hakkında"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Sustur"), ("Audio Input", "Ses Girişi"), ("Enhancements", "Geliştirmeler"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Always connect via relay"), ("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"), ("Login", "Giriş yap"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Çıkış yap"), ("Tags", "Etiketler"), ("Search ID", "ID Arama"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Ölçek uyarlanabilir"), ("General", "Genel"), ("Security", "Güvenlik"), - ("Account", "Hesap"), ("Theme", "Tema"), ("Dark Theme", "Koyu Tema"), ("Dark", "Koyu"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Sunucu"), ("Direct IP Access", "Direk IP Erişimi"), ("Proxy", "Vekil"), - ("Port", "Port"), ("Apply", "Uygula"), ("Disconnect all devices?", "Tüm cihazların bağlantısını kes?"), ("Clear", "Temizle"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Diğer"), ("Confirm before closing multiple tabs", "Çoklu sekmeleri kapatmadan önce onayla"), ("Keyboard Settings", "Klavye Ayarları"), - ("Custom", "Özel"), ("Full Access", "Tam Erişim"), ("Screen Share", "Ekran Paylaşımı"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland, Ubuntu 21.04 veya daha yüksek bir sürüm gerektirir."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 6f0e8806b..a58665a70 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "更改 ID"), ("Website", "網站"), ("About", "關於"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "靜音"), ("Audio Input", "音訊輸入"), ("Enhancements", "增強功能"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "一律透過轉送連線"), ("whitelist_tip", "只有白名單中的 IP 可以存取"), ("Login", "登入"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "登出"), ("Tags", "標籤"), ("Search ID", "搜尋 ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "適應窗口"), ("General", "常規"), ("Security", "安全"), - ("Account", "賬戶"), ("Theme", "主題"), ("Dark Theme", "暗黑主題"), ("Dark", "黑暗"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "服務器"), ("Direct IP Access", "IP直接訪問"), ("Proxy", "代理"), - ("Port", "端口"), ("Apply", "應用"), ("Disconnect all devices?", "斷開所有遠程連接?"), ("Clear", "清空"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "其他"), ("Confirm before closing multiple tabs", "關閉多個分頁前跟我確認"), ("Keyboard Settings", "鍵盤設置"), - ("Custom", "自定義"), ("Full Access", "完全訪問"), ("Screen Share", "僅共享屏幕"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "在只允許密碼連接並且只用固定密碼的情況下才允許隱藏"), ("wayland_experiment_tip", ""), ("Right click to select tabs", "右鍵選擇選項卡"), + ("Skipped", ""), ("Add to Address Book", "添加到地址簿"), + ("Group", "小組"), + ("Search", "搜索"), + ("Closed manually by the web console", "被web控制台手動關閉"), + ("Local keyboard type", "本地鍵盤類型"), + ("Select local keyboard type", "請選擇本地鍵盤類型"), + ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), + ("Always use software rendering", "使用軟件渲染"), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 92fd2db8a..fad7a3880 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -2,8 +2,8 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Статус"), - ("Your Desktop", "Ваш робочий стіл"), - ("desk_tip", "Ваш робочий стіл доступний з цим ідентифікатором і паролем"), + ("Your Desktop", "Ваша стільниця"), + ("desk_tip", "Ваша стільниця доступна з цим ідентифікатором і паролем"), ("Password", "Пароль"), ("Ready", "Готово"), ("Established", "Встановлено"), @@ -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", "Останні сеанси"), @@ -30,7 +30,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("IP Whitelisting", "Список дозволених IP-адрес"), ("ID/Relay Server", "ID/Сервер ретрансляції"), ("Import Server Config", "Імпортувати конфігурацію сервера"), - ("Export Server Config", ""), + ("Export Server Config", "Експортувати конфігурацію сервера"), ("Import server configuration successfully", "Конфігурацію сервера успішно імпортовано"), ("Export server configuration successfully", ""), ("Invalid server configuration", "Недійсна конфігурація сервера"), @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Змінити ID"), ("Website", "Веб-сайт"), ("About", "Про RustDesk"), + ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), + ("Privacy Statement", "Декларація про конфіденційність"), ("Mute", "Вимкнути звук"), ("Audio Input", "Аудіовхід"), ("Enhancements", "Покращення"), @@ -89,8 +91,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Видалити"), ("Properties", "Властивості"), ("Multi Select", "Багатоелементний вибір"), - ("Select All", ""), - ("Unselect All", ""), + ("Select All", "Вибрати все"), + ("Unselect All", "Скасувати вибір"), ("Empty Directory", "Порожня папка"), ("Not an empty directory", "Папка не порожня"), ("Are you sure you want to delete this file?", "Ви впевнені, що хочете видалити цей файл?"), @@ -116,8 +118,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Хороша якість зображення"), ("Balanced", "Збалансований"), ("Optimize reaction time", "Оптимізувати час реакції"), - ("Custom", ""), - ("Show remote cursor", "Показати віддалений курсор"), + ("Custom", "Користувацькі"), + ("Show remote cursor", "Показати віддалений вказівник"), ("Show quality monitor", "Показати якість"), ("Disable clipboard", "Відключити буфер обміну"), ("Lock after session end", "Вихід з облікового запису після завершення сеансу"), @@ -127,13 +129,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID does not exist", "ID не існує"), ("Failed to connect to rendezvous server", "Не вдалося підключитися до проміжного сервера"), ("Please try later", "Будь ласка, спробуйте пізніше"), - ("Remote desktop is offline", "Віддалений робочий стіл не в мережі"), + ("Remote desktop is offline", "Віддалена стільниця не в мережі"), ("Key mismatch", "Невідповідність ключів"), ("Timeout", "Тайм-аут"), ("Failed to connect to relay server", "Не вдалося підключитися до сервера ретрансляції"), ("Failed to connect via rendezvous server", "Не вдалося підключитися через проміжний сервер"), ("Failed to connect via relay server", "Не вдалося підключитися через сервер ретрансляції"), - ("Failed to make direct connection to remote desktop", "Не вдалося встановити пряме підключення до віддаленого робочого столу"), + ("Failed to make direct connection to remote desktop", "Не вдалося встановити пряме підключення до віддаленої стільниці"), ("Set Password", "Встановити пароль"), ("OS Password", "Пароль ОС"), ("install_tip", "У деяких випадках через UAC RustDesk може працювати некоректно на віддаленому вузлі. Щоб уникнути UAC, натисніть кнопку нижче, щоб встановити RustDesk у системі"), @@ -141,16 +143,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to download", "Натисніть, щоб завантажити"), ("Click to update", "Натисніть, щоб оновити"), ("Configure", "Налаштувати"), - ("config_acc", "Щоб віддалено керувати своїм робочим столом, ви повинні надати RustDesk права \"доступу\""), - ("config_screen", "Для віддаленого доступу до робочого столу ви повинні надати RustDesk права \"знімок екрану\""), + ("config_acc", "Щоб віддалено керувати своєю стільницею, ви повинні надати RustDesk права \"доступності\""), + ("config_screen", "Для віддаленого доступу до стільниці ви повинні надати RustDesk права для \"запису екрану\""), ("Installing ...", "Встановлюється..."), ("Install", "Встановити"), ("Installation", "Установка"), ("Installation Path", "Шлях встановлення"), ("Create start menu shortcuts", "Створити ярлики меню \"Пуск\""), - ("Create desktop icon", "Створити значок на робочому столі"), + ("Create desktop icon", "Створити значок на стільниці"), ("agreement_tip", "Починаючи установку, ви приймаєте умови ліцензійної угоди"), - ("Accept and Install", "Прийняти і встановити"), + ("Accept and Install", "Прийняти та встановити"), ("End-user license agreement", "Ліцензійна угода з кінцевим користувачем"), ("Generating ...", "Генерація..."), ("Your installation is lower version.", "Ваша установка більш ранньої версії"), @@ -161,8 +163,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Action", "Дія"), ("Add", "Додати"), ("Local Port", "Локальний порт"), - ("Local Address", ""), - ("Change Local Port", ""), + ("Local Address", "Локальна адреса"), + ("Change Local Port", "Змінити локальний порт"), ("setup_server_tip", "Для більш швидкого підключення налаштуйте свій власний сервер підключення"), ("Too short, at least 6 characters.", "Занадто коротко, мінімум 6 символів"), ("The confirmation is not identical.", "Підтвердження не збігається"), @@ -170,15 +172,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept", "Прийняти"), ("Dismiss", "Відхилити"), ("Disconnect", "Відключити"), - ("Allow using keyboard and mouse", "Дозволити використання клавіатури і миші"), + ("Allow using keyboard and mouse", "Дозволити використання клавіатури та миші"), ("Allow using clipboard", "Дозволити використання буфера обміну"), ("Allow hearing sound", "Дозволити передачу звуку"), - ("Allow file copy and paste", "Дозволити копіювання і вставку файлів"), + ("Allow file copy and paste", "Дозволити копіювання та вставку файлів"), ("Connected", "Підключено"), - ("Direct and encrypted connection", "Пряме і зашифроване з'єднання"), - ("Relayed and encrypted connection", "Ретрансльоване і зашифроване з'єднання"), - ("Direct and unencrypted connection", "Пряме і незашифроване з'єднання"), - ("Relayed and unencrypted connection", "Ретрансльоване і незашифроване з'єднання"), + ("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...", "Вхід..."), @@ -187,7 +189,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Direct IP Access", "Увімкнути прямий IP-доступ"), ("Rename", "Перейменувати"), ("Space", "Місце"), - ("Create Desktop Shortcut", "Створити ярлик на робочому столі"), + ("Create Desktop Shortcut", "Створити ярлик на стільниці"), ("Change Path", "Змінити шлях"), ("Create Folder", "Створити папку"), ("Please enter the folder name", "Будь ласка, введіть ім'я папки"), @@ -195,7 +197,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Попередження"), ("Login screen using Wayland is not supported", "Вхід у систему з використанням Wayland не підтримується"), ("Reboot required", "Потрібне перезавантаження"), - ("Unsupported display server ", ""), + ("Unsupported display server ", "Графічний сервер не підтримується"), ("x11 expected", "Очікується X11"), ("Port", "Порт"), ("Settings", "Налаштування"), @@ -208,11 +210,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Завжди підключатися через ретрансляційний сервер"), ("whitelist_tip", "Тільки IP-адреси з білого списку можуть отримати доступ до мене"), ("Login", "Увійти"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Вийти"), ("Tags", "Ключові слова"), ("Search ID", "Пошук за ID"), - ("Current Wayland display server is not supported", "Поточний сервер відображення Wayland не підтримується"), - ("whitelist_sep", "Окремо комою, крапкою з комою, пропуском або новим рядком"), + ("Current Wayland display server is not supported", "Поточний графічний сервер Wayland не підтримується"), + ("whitelist_sep", "Розділені комою, крапкою з комою, пробілом або новим рядком"), ("Add ID", "Додати ID"), ("Add Tag", "Додати ключове слово"), ("Unselect all tags", "Скасувати вибір усіх тегів"), @@ -244,7 +251,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Two-Finger Tap", "Дотик двома пальцями"), ("Right Mouse", "Права миша"), ("One-Finger Move", "Рух одним пальцем"), - ("Double Tap & Move", "Подвійне натискання і переміщення"), + ("Double Tap & Move", "Подвійне натискання та переміщення"), ("Mouse Drag", "Перетягування мишею"), ("Three-Finger vertically", "Трьома пальцями по вертикалі"), ("Mouse Wheel", "Коліщатко миші"), @@ -272,8 +279,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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_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 або вище."), @@ -300,9 +307,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ignore Battery Optimizations", "Ігнорувати оптимізацію батареї"), ("android_open_battery_optimizations_tip", "Перейдіть на наступну сторінку налаштувань"), ("Connection not allowed", "Підключення не дозволено"), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), + ("Legacy mode", "Застарілий режим"), + ("Map mode", "Режим карти"), + ("Translate mode", "Режим перекладу"), ("Use permanent password", "Використовувати постійний пароль"), ("Use both passwords", "Використовувати обидва паролі"), ("Set permanent password", "Встановити постійний пароль"), @@ -311,13 +318,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restart Remote Device", "Перезапустити віддалений пристрій"), ("Are you sure you want to restart", "Ви впевнені, що хочете виконати перезапуск?"), ("Restarting Remote Device", "Перезавантаження віддаленого пристрою"), - ("remote_restarting_tip", "Віддалений пристрій перезапускається. Будь ласка, закрийте це повідомлення і через деякий час перепідключіться, використовуючи постійний пароль."), + ("remote_restarting_tip", "Віддалений пристрій перезапускається. Будь ласка, закрийте це повідомлення та через деякий час перепідключіться, використовуючи постійний пароль."), ("Copied", ""), ("Exit Fullscreen", "Вийти з повноекранного режиму"), ("Fullscreen", "Повноекранний"), ("Mobile Actions", "Мобільні дії"), ("Select Monitor", "Виберіть монітор"), - ("Control Actions", "Дії з управління"), + ("Control Actions", "Дії для керування"), ("Display Settings", "Налаштування відображення"), ("Ratio", "Співвідношення"), ("Image Quality", "Якість зображення"), @@ -332,20 +339,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Масштаб адаптивний"), ("General", "Загальні"), ("Security", "Безпека"), - ("Account", "Акаунт"), ("Theme", "Тема"), ("Dark Theme", "Темна тема"), ("Dark", "Темна"), ("Light", "Світла"), - ("Follow System", "Використовувати системну"), + ("Follow System", "Як у системі"), ("Enable hardware codec", "Увімкнути апаратний кодек"), ("Unlock Security Settings", "Розблокувати налаштування безпеки"), - ("Enable Audio", "Вімкнути аудіо"), + ("Enable Audio", "Увімкнути аудіо"), ("Unlock Network Settings", "Розблокувати мережеві налаштування"), ("Server", "Сервер"), ("Direct IP Access", "Прямий IP доступ"), ("Proxy", "Проксі"), - ("Port", "Порт"), ("Apply", "Застосувати"), ("Disconnect all devices?", "Відключити всі прилади?"), ("Clear", "Очистити"), @@ -367,38 +372,46 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "Увімкнути пошук локальної мережі"), ("Deny LAN Discovery", "Заборонити виявлення локальної мережі"), ("Write a message", "Написати повідомлення"), - ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), - ("Disconnected", ""), - ("Other", ""), + ("Prompt", "Підказка"), + ("Please wait for confirmation of UAC...", "Будь ласка, зачекайте підтвердження UAC..."), + ("elevated_foreground_window_tip", "Поточне вікно віддаленої стільниці потребує розширених прав для роботи, тому наразі неможливо використати мишу та клавіатуру. Ви можете запропонувати віддаленому користувачу згорнути поточне вікно чи натиснути кнопку розширення прав у вікні керування з'єднаннями. Для уникнення цієї проблеми, рекомендується встановити програму на віддаленому пристрої"), + ("Disconnected", "Відключено"), + ("Other", "Інше"), ("Confirm before closing multiple tabs", ""), - ("Keyboard Settings", ""), - ("Custom", ""), - ("Full Access", ""), - ("Screen Share", ""), + ("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 або змініть свою ОС."), - ("JumpLink", "View"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland потрібна новіша версія дистрибутива Linux. Будь ласка, спробуйте стільницю на X11 або змініть свою ОС."), + ("JumpLink", "Перегляд"), ("Please Select the screen to be shared(Operate on the peer side).", "Будь ласка, виберіть екран, до якого потрібно надати доступ (працюйте на стороні однорангового пристрою)."), - ("Show RustDesk", ""), - ("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", ""), - ("Add to Address Book", ""), + ("Show RustDesk", "Показати RustDesk"), + ("This PC", "Цей ПК"), + ("or", "чи"), + ("Continue with", "Продовжити з"), + ("Elevate", "Розширення прав"), + ("Zoom cursor", "Збільшити вказівник"), + ("Accept sessions via password", "Підтверджувати сеанси паролем"), + ("Accept sessions via click", "Підтверджувати сеанси натисканням"), + ("Accept sessions via both", "Підтверджувати сеанси обома способами"), + ("Please wait for the remote side to accept your session request...", "Буль ласка, зачекайте, поки віддалена сторона підтвердить запит на сеанс..."), + ("One-time Password", "Одноразовий пароль"), + ("Use one-time password", "Використати одноразовий пароль"), + ("One-time password length", "Довжина одноразового пароля"), + ("Request access to your device", "Дати запит щодо доступ до свого пристрою"), + ("Hide connection management window", "Приховати вікно керування з'єднаннями"), + ("hide_cm_tip", "Дозволено приховати лише якщо сеанс підтверджується постійним паролем"), + ("wayland_experiment_tip", "Підтримка Wayland на експериментальній стадії, будь ласка, використовуйте X11, якщо необхідний автоматичний доступ."), + ("Right click to select tabs", "Правий клік для вибору вкладки"), + ("Skipped", ""), + ("Add to Address Book", "Додати IP до Адресної книги"), + ("Group", "Група"), + ("Search", "Пошук"), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 1d32aad5e..187572c83 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Thay đổi ID"), ("Website", "Trang web"), ("About", "About"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Tắt tiếng"), ("Audio Input", "Đầu vào âm thanh"), ("Enhancements", "Các tiện itchs"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Luôn kết nối qua relay"), ("whitelist_tip", "Chỉ có những IP đựoc cho phép mới có thể truy cập"), ("Login", "Đăng nhập"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Đăng xuất"), ("Tags", "Tags"), ("Search ID", "Tìm ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Quy mô thích ứng"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland yêu cầu phiên bản Ubuntu 21.04 trở lên."), @@ -399,6 +403,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index eb8a876ec..7b94c8a2c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #[cfg(not(any(target_os = "ios")))] /// cbindgen:ignore pub mod platform; +mod keyboard; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use platform::{get_cursor, get_cursor_data, get_cursor_pos, start_os_service}; #[cfg(not(any(target_os = "ios")))] diff --git a/src/main.rs b/src/main.rs index 9c7170309..67ddb875f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,8 +36,9 @@ fn main() { use hbb_common::log; let args = format!( "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' + -c, --connect=[REMOTE_ID] 'test only' -k, --key=[KEY] '' - -s, --server... 'Start server'", + -s, --server=[] 'Start server'", ); let matches = App::new("rustdesk") .version(crate::VERSION) @@ -71,6 +72,8 @@ fn main() { if options.len() > 3 { remote_host = options[3].clone(); } + common::test_rendezvous_server(); + common::test_nat_type(); let key = matches.value_of("key").unwrap_or("").to_owned(); let token = LocalConfig::get_option("access_token"); cli::start_one_port_forward( @@ -81,6 +84,14 @@ fn main() { key, token, ); + } else if let Some(p) = matches.value_of("connect") { + common::test_rendezvous_server(); + common::test_nat_type(); + let key = matches.value_of("key").unwrap_or("").to_owned(); + let token = LocalConfig::get_option("access_token"); + cli::connect_test(p, key, token); + } else if let Some(p) = matches.value_of("server") { + crate::start_server(true); } common::global_clean(); } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 82d6592db..3eb8f0b87 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -4,6 +4,7 @@ use hbb_common::{allow_err, bail, log}; use libc::{c_char, c_int, c_void}; use std::{ cell::RefCell, + collections::HashMap, path::PathBuf, sync::{ atomic::{AtomicBool, Ordering}, @@ -179,7 +180,8 @@ fn set_x11_env(uid: &str) { log::info!("uid of seat0: {}", uid); let gdm = format!("/run/user/{}/gdm/Xauthority", uid); let mut auth = get_env_tries("XAUTHORITY", uid, 10); - if auth.is_empty() { + // auth is another user's when uid = 0, https://github.com/rustdesk/rustdesk/issues/2468 + if auth.is_empty() || uid == "0" { auth = if std::path::Path::new(&gdm).exists() { gdm } else { @@ -715,3 +717,84 @@ pub fn get_double_click_time() -> u32 { double_click_time } } + +/// forever: may not work +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 std::process::Command::new(k).args(v).spawn().is_ok() { + return Ok(()); + } + } + bail!("failed to post system message"); +} + +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") + }) { + hbb_common::config::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); + } + log::error!( + "Got signal {} and exit. stack:\n{}", + sig, + stack.join("\n").to_string() + ); + system_message( + "RustDesk", + &format!("Got signal {} and exit.{}", sig, info), + true, + ) + .ok(); + std::process::exit(0); +} + +pub fn register_breakdown_handler() { + unsafe { + libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); + } +} diff --git a/src/platform/macos.mm b/src/platform/macos.mm new file mode 100644 index 000000000..c25a854cc --- /dev/null +++ b/src/platform/macos.mm @@ -0,0 +1,36 @@ +#import +#import +#import + +// https://github.com/codebytere/node-mac-permissions/blob/main/permissions.mm + +extern "C" bool InputMonitoringAuthStatus(bool prompt) { + if (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_15) { + IOHIDAccessType theType = IOHIDCheckAccess(kIOHIDRequestTypeListenEvent); + NSLog(@"IOHIDCheckAccess = %d, kIOHIDAccessTypeGranted = %d", theType, kIOHIDAccessTypeGranted); + switch (theType) { + case kIOHIDAccessTypeGranted: + return true; + break; + case kIOHIDAccessTypeDenied: { + if (prompt) { + NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent"; + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]]; + } + break; + } + case kIOHIDAccessTypeUnknown: { + if (prompt) { + bool result = IOHIDRequestAccess(kIOHIDRequestTypeListenEvent); + NSLog(@"IOHIDRequestAccess result = %d", result); + } + break; + } + default: + break; + } + } else { + return true; + } + return false; +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index edb2aadb1..70e38eb57 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -4,6 +4,7 @@ use super::{CursorData, ResultType}; use cocoa::{ + appkit::{NSApp, NSApplication, NSApplicationActivationPolicy::*}, base::{id, nil, BOOL, NO, YES}, foundation::{NSDictionary, NSPoint, NSSize, NSString}, }; @@ -32,6 +33,7 @@ extern "C" { fn CGEventGetLocation(e: *const c_void) -> CGPoint; static kAXTrustedCheckOptionPrompt: CFStringRef; fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL; + fn InputMonitoringAuthStatus(_: BOOL) -> BOOL; } pub fn is_process_trusted(prompt: bool) -> bool { @@ -47,6 +49,13 @@ pub fn is_process_trusted(prompt: bool) -> bool { } } +pub fn is_can_input_monitoring(prompt: bool) -> bool { + unsafe { + let value = if prompt { YES } else { NO }; + InputMonitoringAuthStatus(value) == YES + } +} + // macOS >= 10.15 // https://stackoverflow.com/questions/56597221/detecting-screen-recording-settings-on-macos-catalina/ // remove just one app from all the permissions: tccutil reset All com.carriez.rustdesk @@ -500,7 +509,7 @@ pub fn start_os_service() { Err(err) => { log::error!("Failed to start server: {}", err); } - _ => { /*no hapen*/ } + _ => { /*no happen*/ } } } std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); @@ -542,3 +551,9 @@ pub fn get_double_click_time() -> u32 { // to-do: https://github.com/servo/core-foundation-rs/blob/786895643140fa0ee4f913d7b4aeb0c4626b2085/cocoa/src/appkit.rs#L2823 500 as _ } + +pub fn hide_dock() { + unsafe { + NSApp().setActivationPolicy_(NSApplicationActivationPolicyAccessory); + } +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index f6b79da59..ed5fcfaa1 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -75,3 +75,4 @@ mod tests { } } } + diff --git a/src/windows.cc b/src/platform/windows.cc similarity index 93% rename from src/windows.cc rename to src/platform/windows.cc index dd3fa2e9e..c4286ebdd 100644 --- a/src/windows.cc +++ b/src/platform/windows.cc @@ -95,7 +95,7 @@ extern "C" CreateEnvironmentBlock(&lpEnvironment, // Environment block hToken, // New token - TRUE); // Inheritence + TRUE); // Inheritance } if (lpEnvironment) { @@ -588,4 +588,44 @@ extern "C" stop_system_key_propagate = v; } + // https://stackoverflow.com/questions/4023586/correct-way-to-find-out-if-a-service-is-running-as-the-system-user + BOOL is_local_system() + { + HANDLE hToken; + UCHAR bTokenUser[sizeof(TOKEN_USER) + 8 + 4 * SID_MAX_SUB_AUTHORITIES]; + PTOKEN_USER pTokenUser = (PTOKEN_USER)bTokenUser; + ULONG cbTokenUser; + SID_IDENTIFIER_AUTHORITY siaNT = SECURITY_NT_AUTHORITY; + PSID pSystemSid; + BOOL bSystem; + + // open process token + if (!OpenProcessToken(GetCurrentProcess(), + TOKEN_QUERY, + &hToken)) + return FALSE; + + // retrieve user SID + if (!GetTokenInformation(hToken, TokenUser, pTokenUser, + sizeof(bTokenUser), &cbTokenUser)) + { + CloseHandle(hToken); + return FALSE; + } + + CloseHandle(hToken); + + // allocate LocalSystem well-known SID + if (!AllocateAndInitializeSid(&siaNT, 1, SECURITY_LOCAL_SYSTEM_RID, + 0, 0, 0, 0, 0, 0, 0, &pSystemSid)) + return FALSE; + + // compare the user SID from the token with the LocalSystem SID + bSystem = EqualSid(pTokenUser->User.Sid, pSystemSid); + + FreeSid(pSystemSid); + + return bSystem; + } + } // end of extern "C" \ No newline at end of file diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 075f7ed08..a2a99800f 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -439,6 +439,7 @@ extern "C" { fn win32_disable_lowlevel_keyboard(hwnd: HWND); fn win_stop_system_key_propagate(v: BOOL); fn is_win_down() -> BOOL; + fn is_local_system() -> BOOL; } extern "system" { @@ -718,10 +719,10 @@ pub fn set_share_rdp(enable: bool) { } pub fn get_active_username() -> String { - let name = crate::username(); - if name != "SYSTEM" { - return name; + if !is_root() { + return crate::username(); } + extern "C" { fn get_active_user(path: *mut u16, n: u32, rdp: BOOL) -> u32; } @@ -757,7 +758,8 @@ pub fn is_prelogin() -> bool { } pub fn is_root() -> bool { - crate::username() == "SYSTEM" + // https://stackoverflow.com/questions/4023586/correct-way-to-find-out-if-a-service-is-running-as-the-system-user + unsafe { is_local_system() == TRUE } } pub fn lock_screen() { diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 9350085c4..73c017e2e 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -18,13 +18,14 @@ use hbb_common::{ log, protobuf::Message as _, rendezvous_proto::*, - sleep, socket_client, + sleep, + socket_client::{self, is_ipv4}, tokio::{ self, select, time::{interval, Duration}, }, udp::FramedSocket, - AddrMangle, IntoTargetAddr, ResultType, TargetAddr, + AddrMangle, ResultType, }; use crate::server::{check_zombie, new as new_server, ServerPtr}; @@ -38,7 +39,7 @@ static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); #[derive(Clone)] pub struct RendezvousMediator { - addr: TargetAddr<'static>, + addr: hbb_common::tokio_socks::TargetAddr<'static>, host: String, host_prefix: String, last_id_pk_registry: String, @@ -110,17 +111,15 @@ impl RendezvousMediator { } }) .unwrap_or(host.to_owned()); + let host = crate::check_port(&host, RENDEZVOUS_PORT); + let (mut socket, addr) = socket_client::new_udp_for(&host, RENDEZVOUS_TIMEOUT).await?; let mut rz = Self { - addr: Config::get_any_listen_addr().into_target_addr()?, + addr: addr, host: host.clone(), host_prefix, last_id_pk_registry: "".to_owned(), }; - rz.addr = socket_client::get_target_addr(&crate::check_port(&host, RENDEZVOUS_PORT))?; - let any_addr = Config::get_any_listen_addr(); - let mut socket = socket_client::new_udp(any_addr, RENDEZVOUS_TIMEOUT).await?; - const TIMER_OUT: Duration = Duration::from_secs(1); let mut timer = interval(TIMER_OUT); let mut last_timer: Option = None; @@ -250,11 +249,11 @@ impl RendezvousMediator { Config::update_latency(&host, -1); old_latency = 0; if last_dns_check.elapsed().as_millis() as i64 > DNS_INTERVAL { - rz.addr = socket_client::get_target_addr(&crate::check_port(&host, RENDEZVOUS_PORT))?; // in some case of network reconnect (dial IP network), // old UDP socket not work any more after network recover - if let Some(s) = socket_client::rebind_udp(any_addr).await? { + if let Some((s, addr)) = socket_client::rebind_udp_for(&rz.host).await? { socket = s; + rz.addr = addr; } last_dns_check = Instant::now(); } @@ -293,19 +292,14 @@ impl RendezvousMediator { ) -> ResultType<()> { let peer_addr = AddrMangle::decode(&socket_addr); log::info!( - "create_relay requested from from {:?}, relay_server: {}, uuid: {}, secure: {}", + "create_relay requested from {:?}, relay_server: {}, uuid: {}, secure: {}", peer_addr, relay_server, uuid, secure, ); - let mut socket = socket_client::connect_tcp( - self.addr.to_owned(), - Config::get_any_listen_addr(), - RENDEZVOUS_TIMEOUT, - ) - .await?; + let mut socket = socket_client::connect_tcp(&*self.host, RENDEZVOUS_TIMEOUT).await?; let mut msg_out = Message::new(); let mut rr = RelayResponse { @@ -320,24 +314,41 @@ impl RendezvousMediator { } msg_out.set_relay_response(rr); socket.send(&msg_out).await?; - crate::create_relay_connection(server, relay_server, uuid, peer_addr, secure).await; + crate::create_relay_connection( + server, + relay_server, + uuid, + peer_addr, + secure, + is_ipv4(&self.addr), + ) + .await; Ok(()) } async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> { + let relay_server = self.get_relay_server(fla.relay_server); + if !is_ipv4(&self.addr) { + // nat64, go relay directly, because current hbbs will crash if demangle ipv6 address + let uuid = Uuid::new_v4().to_string(); + return self + .create_relay( + fla.socket_addr.into(), + relay_server, + uuid, + server, + true, + true, + ) + .await; + } let peer_addr = AddrMangle::decode(&fla.socket_addr); log::debug!("Handle intranet from {:?}", peer_addr); - let mut socket = socket_client::connect_tcp( - self.addr.to_owned(), - Config::get_any_listen_addr(), - RENDEZVOUS_TIMEOUT, - ) - .await?; + let mut socket = socket_client::connect_tcp(&*self.host, RENDEZVOUS_TIMEOUT).await?; let local_addr = socket.local_addr(); let local_addr: SocketAddr = format!("{}:{}", local_addr.ip(), local_addr.port()).parse()?; let mut msg_out = Message::new(); - let relay_server = self.get_relay_server(fla.relay_server); msg_out.set_local_addr(LocalAddr { id: Config::get_id(), socket_addr: AddrMangle::encode(peer_addr).into(), @@ -372,14 +383,11 @@ impl RendezvousMediator { let peer_addr = AddrMangle::decode(&ph.socket_addr); log::debug!("Punch hole to {:?}", peer_addr); let mut socket = { - let socket = socket_client::connect_tcp( - self.addr.to_owned(), - Config::get_any_listen_addr(), - RENDEZVOUS_TIMEOUT, - ) - .await?; + let socket = socket_client::connect_tcp(&*self.host, RENDEZVOUS_TIMEOUT).await?; let local_addr = socket.local_addr(); - allow_err!(socket_client::connect_tcp(peer_addr, local_addr, 300).await); + // key important here for punch hole to tell my gateway incoming peer is safe. + // it can not be async here, because local_addr can not be reused, we must close the connection before use it again. + allow_err!(socket_client::connect_tcp_local(peer_addr, Some(local_addr), 30).await); socket }; let mut msg_out = Message::new(); @@ -467,15 +475,7 @@ impl RendezvousMediator { relay_server = provided_by_rendzvous_server; } if relay_server.is_empty() { - if self.host.contains(":") { - let tmp: Vec<&str> = self.host.split(":").collect(); - if tmp.len() == 2 { - let port: u16 = tmp[1].parse().unwrap_or(0); - relay_server = format!("{}:{}", tmp[0], port + 1); - } - } else { - relay_server = self.host.clone(); - } + relay_server = crate::increase_port(&self.host, 1); } relay_server } @@ -498,8 +498,7 @@ async fn direct_server(server: ServerPtr) { let disabled = Config::get_option("direct-server").is_empty(); if !disabled && listener.is_none() { port = get_direct_port(); - let addr = format!("0.0.0.0:{}", port); - match hbb_common::tcp::new_listener(&addr, false).await { + match hbb_common::tcp::listen_any(port as _).await { Ok(l) => { listener = Some(l); log::info!( @@ -510,8 +509,8 @@ async fn direct_server(server: ServerPtr) { Err(err) => { // to-do: pass to ui log::error!( - "Failed to start direct server on : {}, error: {}", - addr, + "Failed to start direct server on port: {}, error: {}", + port, err ); loop { @@ -532,7 +531,9 @@ async fn direct_server(server: ServerPtr) { if let Ok(Ok((stream, addr))) = hbb_common::timeout(1000, l.accept()).await { stream.set_nodelay(true).ok(); log::info!("direct access from {}", addr); - let local_addr = stream.local_addr().unwrap_or(Config::get_any_listen_addr()); + let local_addr = stream + .local_addr() + .unwrap_or(Config::get_any_listen_addr(true)); let server = server.clone(); tokio::spawn(async move { allow_err!( @@ -650,13 +651,7 @@ async fn create_online_stream() -> ResultType { bail!("Invalid server address: {}", rendezvous_server); } let online_server = format!("{}:{}", tmp[0], port - 1); - let server_addr = socket_client::get_target_addr(&online_server)?; - socket_client::connect_tcp( - server_addr, - Config::get_any_listen_addr(), - RENDEZVOUS_TIMEOUT, - ) - .await + socket_client::connect_tcp(online_server, RENDEZVOUS_TIMEOUT).await } async fn query_online_states_( diff --git a/src/server.rs b/src/server.rs index d08dd2672..5c020261f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -85,7 +85,7 @@ pub fn new() -> ServerPtr { #[cfg(not(any(target_os = "android", target_os = "ios")))] { server.add_service(Box::new(clipboard_service::new())); - if !video_service::capture_cursor_embeded() { + if !video_service::capture_cursor_embedded() { server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_pos())); } @@ -194,6 +194,11 @@ pub async fn create_tcp_connection( } } + #[cfg(target_os = "macos")]{ + use std::process::Command; + Command::new("/usr/bin/caffeinate").arg("-u").arg("-t 5").spawn().ok(); + log::info!("wake up macos"); + } Connection::start(addr, stream, id, Arc::downgrade(&server)).await; Ok(()) } @@ -215,9 +220,10 @@ pub async fn create_relay_connection( uuid: String, peer_addr: SocketAddr, secure: bool, + ipv4: bool, ) { if let Err(err) = - create_relay_connection_(server, relay_server, uuid.clone(), peer_addr, secure).await + create_relay_connection_(server, relay_server, uuid.clone(), peer_addr, secure, ipv4).await { log::error!( "Failed to create relay connection for {} with uuid {}: {}", @@ -234,10 +240,10 @@ async fn create_relay_connection_( uuid: String, peer_addr: SocketAddr, secure: bool, + ipv4: bool, ) -> ResultType<()> { let mut stream = socket_client::connect_tcp( - crate::check_port(relay_server, RELAY_PORT), - Config::get_any_listen_addr(), + socket_client::ipv4_to_ipv6(crate::check_port(relay_server, RELAY_PORT), ipv4), CONNECT_TIMEOUT, ) .await?; diff --git a/src/server/connection.rs b/src/server/connection.rs index fdd0ea77a..f91281a52 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -26,6 +26,7 @@ use hbb_common::{ }; #[cfg(any(target_os = "android", target_os = "ios"))] use scrap::android::call_main_service_mouse_input; +use serde::Deserialize; use serde_json::{json, value::Value}; use sha2::{Digest, Sha256}; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -72,6 +73,8 @@ pub struct Connection { hash: Hash, read_jobs: Vec, timer: Interval, + file_timer: Interval, + http_timer: Interval, file_transfer: Option<(String, bool)>, port_forward_socket: Option>, port_forward_address: String, @@ -93,7 +96,8 @@ pub struct Connection { tx_input: std_mpsc::Sender, // handle input messages video_ack_required: bool, peer_info: (String, String), - api_server: String, + server_audit_conn: String, + server_audit_file: String, lr: LoginRequest, last_recv_time: Arc>, chat_unanswered: bool, @@ -150,6 +154,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_stop, mut rx_stop) = mpsc::unbounded_channel::(); let tx_cloned = tx.clone(); let mut conn = Self { @@ -163,6 +168,8 @@ impl Connection { hash, read_jobs: Vec::new(), timer: time::interval(SEC30), + file_timer: time::interval(SEC30), + http_timer: time::interval(Duration::from_secs(3)), file_transfer: None, port_forward_socket: None, port_forward_address: "".to_owned(), @@ -184,7 +191,8 @@ impl Connection { tx_input, video_ack_required: false, peer_info: Default::default(), - api_server: "".to_owned(), + server_audit_conn: "".to_owned(), + server_audit_file: "".to_owned(), lr: Default::default(), last_recv_time: Arc::new(Mutex::new(Instant::now())), chat_unanswered: false, @@ -244,7 +252,7 @@ impl Connection { loop { tokio::select! { - // biased; // video has higher priority // causing test_delay_timer failed while transfering big file + // biased; // video has higher priority // causing test_delay_timer failed while transferring big file Some(data) = rx_from_cm.recv() => { match data { @@ -255,7 +263,7 @@ impl Connection { } } ipc::Data::Close => { - conn.on_close_manually("connection manager").await; + conn.on_close_manually("connection manager", "peer").await; break; } ipc::Data::ChatMessage{text} => { @@ -375,20 +383,26 @@ impl Connection { break; } }, - _ = conn.timer.tick() => { + _ = conn.file_timer.tick() => { if !conn.read_jobs.is_empty() { if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await { conn.on_close(&err.to_string(), false).await; break; } } else { - conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); + conn.file_timer = time::interval_at(Instant::now() + SEC30, SEC30); } - conn.post_audit(json!({})); // heartbeat + } + _ = conn.http_timer.tick() => { + Connection::post_heartbeat(conn.server_audit_conn.clone(), conn.inner.id, tx_stop.clone()); }, + Some(reason) = rx_stop.recv() => { + conn.on_close_manually(&reason, &reason).await; + + } Some((instant, value)) = rx_video.recv() => { if !conn.video_ack_required { - video_service::notify_video_frame_feched(id, Some(instant.into())); + video_service::notify_video_frame_fetched(id, Some(instant.into())); } if let Err(err) = conn.stream.send(&value as &Message).await { conn.on_close(&err.to_string(), false).await; @@ -412,7 +426,7 @@ impl Connection { Some(message::Union::Misc(m)) => { match &m.union { Some(misc::Union::StopService(_)) => { - conn.on_close_manually("stop service").await; + conn.on_close_manually("stop service", "peer").await; break; } _ => {}, @@ -487,7 +501,7 @@ impl Connection { } else if video_privacy_conn_id == 0 { let _ = privacy_mode::turn_off_privacy(0); } - video_service::notify_video_frame_feched(id, None); + video_service::notify_video_frame_fetched(id, None); scrap::codec::Encoder::update_video_encoder(id, scrap::codec::EncoderUpdate::Remove); video_service::VIDEO_QOS.lock().unwrap().reset(); if conn.authorized { @@ -497,7 +511,7 @@ impl Connection { conn.on_close(&err.to_string(), false).await; } - conn.post_audit(json!({ + conn.post_conn_audit(json!({ "action": "close", })); log::info!("#{} connection loop exited", id); @@ -511,6 +525,8 @@ impl Connection { rdev::set_dw_mouse_extra_info(enigo::ENIGO_INPUT_EXTRA_VALUE); rdev::set_dw_keyboard_extra_info(enigo::ENIGO_INPUT_EXTRA_VALUE); } + #[cfg(target_os = "macos")] + reset_input_ondisconn(); loop { match receiver.recv_timeout(std::time::Duration::from_millis(500)) { Ok(v) => match v { @@ -568,6 +584,7 @@ impl Connection { rx_from_cm: &mut mpsc::UnboundedReceiver, ) -> ResultType<()> { let mut last_recv_time = Instant::now(); + let (tx_stop, mut rx_stop) = mpsc::unbounded_channel::(); if let Some(mut forward) = self.port_forward_socket.take() { log::info!("Running port forwarding loop"); self.stream.set_raw(); @@ -601,7 +618,10 @@ impl Connection { if last_recv_time.elapsed() >= H1 { bail!("Timeout"); } - self.post_audit(json!({})); // heartbeat + Connection::post_heartbeat(self.server_audit_conn.clone(), self.inner.id, tx_stop.clone()); + } + Some(reason) = rx_stop.recv() => { + bail!(reason); } } } @@ -642,6 +662,13 @@ impl Connection { { self.send_login_error("Your ip is blocked by the peer") .await; + Self::post_alarm_audit( + AlarmAuditType::IpWhiltelist, //"ip whiltelist", + true, + json!({ + "ip":addr.ip(), + }), + ); sleep(1.).await; return false; } @@ -650,7 +677,7 @@ impl Connection { msg_out.set_hash(self.hash.clone()); self.send(msg_out).await; self.get_api_server(); - self.post_audit(json!({ + self.post_conn_audit(json!({ "ip": addr.ip(), "action": "new", })); @@ -658,30 +685,114 @@ impl Connection { } fn get_api_server(&mut self) { - self.api_server = crate::get_audit_server( + self.server_audit_conn = crate::get_audit_server( Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"), + "conn".to_owned(), + ); + self.server_audit_file = crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + "file".to_owned(), ); } - fn post_audit(&self, v: Value) { - if self.api_server.is_empty() { + fn post_conn_audit(&self, v: Value) { + if self.server_audit_conn.is_empty() { return; } - let url = self.api_server.clone(); + let url = self.server_audit_conn.clone(); let mut v = v; v["id"] = json!(Config::get_id()); v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); - v["Id"] = json!(self.inner.id); + v["conn_id"] = json!(self.inner.id); + tokio::spawn(async move { + allow_err!(Self::post_audit_async(url, v).await); + }); + } + + fn post_heartbeat( + server_audit_conn: String, + conn_id: i32, + tx_stop: mpsc::UnboundedSender, + ) { + if server_audit_conn.is_empty() { + return; + } + let url = server_audit_conn.clone(); + let mut v = Value::default(); + v["id"] = json!(Config::get_id()); + v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); + v["conn_id"] = json!(conn_id); + tokio::spawn(async move { + if let Ok(rsp) = Self::post_audit_async(url, v).await { + if let Ok(rsp) = serde_json::from_str::(&rsp) { + if rsp.action == "disconnect" { + tx_stop.send("web console".to_string()).ok(); + } + } + } + }); + } + + fn post_file_audit( + &self, + r#type: FileAuditType, + path: &str, + files: Vec<(String, i64)>, + info: Value, + ) { + if self.server_audit_file.is_empty() { + return; + } + let url = self.server_audit_file.clone(); + let file_num = files.len(); + let mut files = files; + files.sort_by(|a, b| b.1.cmp(&a.1)); + files.truncate(10); + let is_file = files.len() == 1 && files[0].0.is_empty(); + let mut info = info; + info["ip"] = json!(self.ip.clone()); + info["name"] = json!(self.lr.my_name.clone()); + info["num"] = json!(file_num); + info["files"] = json!(files); + let v = json!({ + "id":json!(Config::get_id()), + "uuid":json!(base64::encode(hbb_common::get_uuid())), + "peer_id":json!(self.lr.my_id), + "type": r#type as i8, + "path":path, + "is_file":is_file, + "info":json!(info).to_string(), + }); + tokio::spawn(async move { + allow_err!(Self::post_audit_async(url, v).await); + }); + } + + pub fn post_alarm_audit(typ: AlarmAuditType, from_remote: bool, info: Value) { + let url = crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + "alarm".to_owned(), + ); + if url.is_empty() { + return; + } + let mut v = Value::default(); + v["id"] = json!(Config::get_id()); + v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); + v["typ"] = json!(typ as i8); + v["from_remote"] = json!(from_remote); + v["info"] = serde_json::Value::String(info.to_string()); tokio::spawn(async move { allow_err!(Self::post_audit_async(url, v).await); }); } #[inline] - async fn post_audit_async(url: String, v: Value) -> ResultType<()> { - crate::post_request(url, v.to_string(), "").await?; - Ok(()) + async fn post_audit_async(url: String, v: Value) -> ResultType { + crate::post_request(url, v.to_string(), "").await } async fn send_logon_response(&mut self) { @@ -695,7 +806,7 @@ impl Connection { } else { 0 }; - self.post_audit(json!({"peer": self.peer_info, "Type": conn_type})); + self.post_conn_audit(json!({"peer": self.peer_info, "type": conn_type})); #[allow(unused_mut)] let mut username = crate::platform::get_active_username(); let mut res = LoginResponse::new(); @@ -1050,7 +1161,7 @@ impl Connection { } _ => {} } - if !crate::is_ip(&lr.username) && lr.username != Config::get_id() { + if !hbb_common::is_ipv4_str(&lr.username) && lr.username != Config::get_id() { self.send_login_error("Offline").await; } else if password::approve_mode() == ApproveMode::Click || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() @@ -1086,8 +1197,22 @@ impl Connection { if failure.2 > 30 { self.send_login_error("Too many wrong password attempts") .await; + Self::post_alarm_audit( + AlarmAuditType::ManyWrongPassword, + true, + json!({ + "ip":self.ip, + }), + ); } else if time == failure.0 && failure.1 > 6 { self.send_login_error("Please try 1 minute later").await; + Self::post_alarm_audit( + AlarmAuditType::FrequentAttempt, + true, + json!({ + "ip":self.ip, + }), + ); } else if !self.validate_password() { if failure.0 == time { failure.1 += 1; @@ -1225,8 +1350,18 @@ impl Connection { Ok(job) => { self.send(fs::new_dir(id, path, job.files().to_vec())) .await; + let mut files = job.files().to_owned(); self.read_jobs.push(job); - self.timer = time::interval(MILLI1); + self.file_timer = time::interval(MILLI1); + self.post_file_audit( + FileAuditType::RemoteSend, + &s.path, + files + .drain(..) + .map(|f| (f.name, f.size as _)) + .collect(), + json!({}), + ); } } } @@ -1237,7 +1372,7 @@ impl Connection { &self.lr.version, )); self.send_fs(ipc::FS::NewWrite { - path: r.path, + path: r.path.clone(), id: r.id, file_num: r.file_num, files: r @@ -1248,6 +1383,16 @@ impl Connection { .collect(), overwrite_detection: od, }); + self.post_file_audit( + FileAuditType::RemoteReceive, + &r.path, + r.files + .to_vec() + .drain(..) + .map(|f| (f.name, f.size as _)) + .collect(), + json!({}), + ); } Some(file_action::Union::RemoveDir(d)) => { self.send_fs(ipc::FS::RemoveDir { @@ -1330,7 +1475,7 @@ impl Connection { } } Some(misc::Union::VideoReceived(_)) => { - video_service::notify_video_frame_feched( + video_service::notify_video_frame_fetched( self.inner.id, Some(Instant::now().into()), ); @@ -1541,10 +1686,10 @@ impl Connection { self.port_forward_socket.take(); } - async fn on_close_manually(&mut self, close_from: &str) { + async fn on_close_manually(&mut self, close_from: &str, close_by: &str) { self.close_manually = true; let mut misc = Misc::new(); - misc.set_close_reason("Closed manually by the peer".into()); + misc.set_close_reason(format!("Closed manually by the {}", close_by)); let mut msg_out = Message::new(); msg_out.set_misc(misc); self.send(msg_out).await; @@ -1721,3 +1866,21 @@ mod privacy_mode { } } } + +#[derive(Debug, Deserialize)] +struct ConnAuditResponse { + #[allow(dead_code)] + ret: bool, + action: String, +} + +pub enum AlarmAuditType { + IpWhiltelist = 0, + ManyWrongPassword = 1, + FrequentAttempt = 2, +} + +pub enum FileAuditType { + RemoteSend = 0, + RemoteReceive = 1, +} diff --git a/src/server/input_service.rs b/src/server/input_service.rs index b465658bb..41ce8fd9e 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -5,7 +5,10 @@ use crate::common::IS_X11; use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; use hbb_common::{config::COMPRESS_LEVEL, get_time, protobuf::EnumOrUnknown}; -use rdev::{simulate, EventType, Key as RdevKey}; +use rdev::{self, EventType, Key as RdevKey, RawKey}; +#[cfg(target_os = "macos")] +use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; +use std::time::Duration; use std::{ convert::TryFrom, ops::Sub, @@ -68,6 +71,7 @@ struct Input { y: i32, } +const KEY_RDEV_START: u64 = 999; const KEY_CHAR_START: u64 = 9999; #[derive(Clone, Default)] @@ -219,9 +223,14 @@ lazy_static::lazy_static! { static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned()); } +#[cfg(target_os = "macos")] +static mut VIRTUAL_INPUT_MTX: Mutex<()> = Mutex::new(()); +#[cfg(target_os = "macos")] +static mut VIRTUAL_INPUT: Option = None; + // First call set_uinput() will create keyboard and mouse clients. // The clients are ipc connections that must live shorter than tokio runtime. -// Thus this funtion must not be called in a temporary runtime. +// Thus this function must not be called in a temporary runtime. #[cfg(target_os = "linux")] pub async fn setup_uinput(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { // Keyboard and mouse both open /dev/uinput @@ -245,16 +254,18 @@ pub async fn setup_uinput(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultT pub async fn update_mouse_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { set_uinput_resolution(minx, maxx, miny, maxy).await?; - if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() { - if let Some(mouse) = mouse - .as_mut_any() - .downcast_mut::() - { - allow_err!(mouse.send_refresh()); - } else { - log::error!("failed downcast uinput mouse"); + std::thread::spawn(|| { + if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() { + if let Some(mouse) = mouse + .as_mut_any() + .downcast_mut::() + { + allow_err!(mouse.send_refresh()); + } else { + log::error!("failed downcast uinput mouse"); + } } - } + }); Ok(()) } @@ -277,13 +288,18 @@ pub fn mouse_move_relative(x: i32, y: i32) { en.mouse_move_relative(x, y); } -#[cfg(not(target_os = "macos"))] +#[cfg(windows)] fn modifier_sleep() { // sleep for a while, this is only for keying in rdp in peer so far - #[cfg(windows)] std::thread::sleep(std::time::Duration::from_nanos(1)); } +#[inline] +#[cfg(not(target_os = "macos"))] +fn is_pressed(key: &Key, en: &mut Enigo) -> bool { + get_modifier_state(key.clone(), en) +} + #[inline] fn get_modifier_state(key: Key, en: &mut Enigo) -> bool { // https://github.com/rustdesk/rustdesk/issues/332 @@ -332,7 +348,7 @@ pub fn handle_mouse(evt: &MouseEvent, conn: i32) { pub fn fix_key_down_timeout_loop() { std::thread::spawn(move || loop { - std::thread::sleep(std::time::Duration::from_millis(1_000)); + std::thread::sleep(std::time::Duration::from_millis(10_000)); fix_key_down_timeout(false); }); if let Err(err) = ctrlc::set_handler(move || { @@ -353,38 +369,63 @@ pub fn fix_key_down_timeout_at_exit() { } #[inline] -fn get_layout(key: u32) -> Key { - Key::Layout(std::char::from_u32(key).unwrap_or('\0')) +fn record_key_is_control_key(record_key: u64) -> bool { + record_key < KEY_CHAR_START +} + +#[inline] +fn record_key_is_chr(record_key: u64) -> bool { + KEY_RDEV_START <= record_key && record_key < KEY_CHAR_START +} + +#[inline] +fn record_key_is_rdev_layout(record_key: u64) -> bool { + KEY_CHAR_START <= record_key +} + +#[inline] +fn record_key_to_key(record_key: u64) -> Option { + if record_key_is_control_key(record_key) { + control_key_value_to_key(record_key as _) + } else if record_key_is_chr(record_key) { + let chr: u32 = (record_key - KEY_CHAR_START) as _; + Some(char_value_to_key(chr)) + } else { + None + } +} + +#[inline] +fn release_record_key(record_key: u64) { + let func = move || { + if record_key_is_rdev_layout(record_key) { + simulate_(&EventType::KeyRelease(RdevKey::Unknown( + (record_key - KEY_RDEV_START) as _, + ))); + } else if let Some(key) = record_key_to_key(record_key) { + ENIGO.lock().unwrap().key_up(key); + log::debug!("Fixed {:?} timeout", key); + } + }; + + #[cfg(target_os = "macos")] + QUEUE.exec_async(func); + #[cfg(not(target_os = "macos"))] + func(); } fn fix_key_down_timeout(force: bool) { - if KEYS_DOWN.lock().unwrap().is_empty() { + let key_down = KEYS_DOWN.lock().unwrap(); + if key_down.is_empty() { return; } - let cloned = (*KEYS_DOWN.lock().unwrap()).clone(); - for (key, value) in cloned.into_iter() { - if force || value.elapsed().as_millis() >= 360_000 { - KEYS_DOWN.lock().unwrap().remove(&key); - let key = if key < KEY_CHAR_START { - if let Some(key) = KEY_MAP.get(&(key as _)) { - Some(*key) - } else { - None - } - } else { - Some(get_layout((key - KEY_CHAR_START) as _)) - }; - if let Some(key) = key { - let func = move || { - let mut en = ENIGO.lock().unwrap(); - en.key_up(key); - log::debug!("Fixed {:?} timeout", key); - }; - #[cfg(target_os = "macos")] - QUEUE.exec_async(func); - #[cfg(not(target_os = "macos"))] - func(); - } + let cloned = (*key_down).clone(); + drop(key_down); + + for (record_key, time) in cloned.into_iter() { + if force || time.elapsed().as_millis() >= 360_000 { + record_pressed_key(record_key, false); + release_record_key(record_key); } } } @@ -502,6 +543,7 @@ pub fn handle_mouse_(evt: &MouseEvent) { if key != &Key::CapsLock && key != &Key::NumLock { if !get_modifier_state(key.clone(), &mut en) { en.key_down(key.clone()).ok(); + #[cfg(windows)] modifier_sleep(); to_release.push(key); } @@ -514,27 +556,39 @@ pub fn handle_mouse_(evt: &MouseEvent) { en.mouse_move_to(evt.x, evt.y); } 1 => match buttons { - 1 => { + 0x01 => { allow_err!(en.mouse_down(MouseButton::Left)); } - 2 => { + 0x02 => { allow_err!(en.mouse_down(MouseButton::Right)); } - 4 => { + 0x04 => { allow_err!(en.mouse_down(MouseButton::Middle)); } + 0x08 => { + allow_err!(en.mouse_down(MouseButton::Back)); + } + 0x10 => { + allow_err!(en.mouse_down(MouseButton::Forward)); + } _ => {} }, 2 => match buttons { - 1 => { + 0x01 => { en.mouse_up(MouseButton::Left); } - 2 => { + 0x02 => { en.mouse_up(MouseButton::Right); } - 4 => { + 0x04 => { en.mouse_up(MouseButton::Middle); } + 0x08 => { + en.mouse_up(MouseButton::Back); + } + 0x10 => { + en.mouse_up(MouseButton::Forward); + } _ => {} }, 3 | 4 => { @@ -640,7 +694,423 @@ pub async fn lock_screen() { super::video_service::switch_to_primary().await; } +pub fn handle_key(evt: &KeyEvent) { + #[cfg(target_os = "macos")] + if !*IS_SERVER { + // having GUI, run main GUI thread, otherwise crash + let evt = evt.clone(); + QUEUE.exec_async(move || handle_key_(&evt)); + std::thread::sleep(Duration::from_millis(20)); + return; + } + #[cfg(windows)] + crate::portable_service::client::handle_key(evt); + #[cfg(not(windows))] + handle_key_(evt); + #[cfg(target_os = "macos")] + std::thread::sleep(Duration::from_millis(20)); +} + +#[cfg(target_os = "macos")] +#[inline] +fn reset_input() { + unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); + VIRTUAL_INPUT = VirtualInput::new( + CGEventSourceStateID::Private, + CGEventTapLocation::AnnotatedSession, + ) + .ok(); + } +} + +#[cfg(target_os = "macos")] +pub fn reset_input_ondisconn() { + if !*IS_SERVER { + QUEUE.exec_async(reset_input); + } else { + reset_input(); + } +} + +fn sim_rdev_rawkey(code: u32, keydown: bool) { + #[cfg(target_os = "windows")] + let rawkey = RawKey::ScanCode(code); + #[cfg(target_os = "linux")] + let rawkey = RawKey::LinuxXorgKeycode(code); + // // to-do: test android + // #[cfg(target_os = "android")] + // let rawkey = RawKey::LinuxConsoleKeycode(code); + #[cfg(target_os = "macos")] + let rawkey = RawKey::MacVirtualKeycode(code); + + let event_type = if keydown { + EventType::KeyPress(RdevKey::RawKey(rawkey)) + } else { + EventType::KeyRelease(RdevKey::RawKey(rawkey)) + }; + simulate_(&event_type); +} + +#[cfg(target_os = "macos")] +#[inline] +fn simulate_(event_type: &EventType) { + unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); + if let Some(virtual_input) = &VIRTUAL_INPUT { + let _ = virtual_input.simulate(&event_type); + } + } +} + +#[cfg(not(target_os = "macos"))] +#[inline] +fn simulate_(event_type: &EventType) { + match rdev::simulate(&event_type) { + Ok(()) => (), + Err(_simulate_error) => { + log::error!("Could not send {:?}", &event_type); + } + } +} + +fn is_modifier_in_key_event(control_key: ControlKey, key_event: &KeyEvent) -> bool { + key_event + .modifiers + .iter() + .position(|&m| m == control_key.into()) + .is_some() +} + +#[inline] +fn control_key_value_to_key(value: i32) -> Option { + KEY_MAP.get(&value).and_then(|k| Some(*k)) +} + +#[inline] +fn char_value_to_key(value: u32) -> Key { + Key::Layout(std::char::from_u32(value).unwrap_or('\0')) +} + +fn is_not_same_status(client_locking: bool, remote_locking: bool) -> bool { + client_locking != remote_locking +} + +#[cfg(target_os = "windows")] +fn has_numpad_key(key_event: &KeyEvent) -> bool { + key_event + .modifiers + .iter() + .filter(|&&ck| NUMPAD_KEY_MAP.get(&ck.value()).is_some()) + .count() + != 0 +} + +#[cfg(target_os = "windows")] +fn is_rdev_numpad_key(key_event: &KeyEvent) -> bool { + let code = key_event.chr(); + let key = rdev::get_win_key(code, 0); + match key { + RdevKey::Home + | RdevKey::UpArrow + | RdevKey::PageUp + | RdevKey::LeftArrow + | RdevKey::RightArrow + | RdevKey::End + | RdevKey::DownArrow + | RdevKey::PageDown + | RdevKey::Insert + | RdevKey::Delete => true, + _ => false, + } +} + +#[cfg(target_os = "windows")] +fn is_numlock_disabled(key_event: &KeyEvent) -> bool { + // disable numlock if press home etc when numlock is on, + // because we will get numpad value (7,8,9 etc) if not + match key_event.mode.unwrap() { + KeyboardMode::Map => is_rdev_numpad_key(key_event), + _ => has_numpad_key(key_event), + } +} + +fn click_capslock(en: &mut Enigo) { + #[cfg(not(targe_os = "macos"))] + en.key_click(enigo::Key::CapsLock); + #[cfg(target_os = "macos")] + let _ = en.key_down(enigo::Key::CapsLock); +} + +fn click_numlock(_en: &mut Enigo) { + // without numlock in macos + #[cfg(not(target_os = "macos"))] + _en.key_click(enigo::Key::NumLock); +} + +fn sync_numlock_capslock_status(key_event: &KeyEvent) { + let mut en = ENIGO.lock().unwrap(); + + let client_caps_locking = is_modifier_in_key_event(ControlKey::CapsLock, key_event); + let client_num_locking = is_modifier_in_key_event(ControlKey::NumLock, key_event); + let remote_caps_locking = en.get_key_state(enigo::Key::CapsLock); + let remote_num_locking = en.get_key_state(enigo::Key::NumLock); + + let need_click_capslock = is_not_same_status(client_caps_locking, remote_caps_locking); + let need_click_numlock = is_not_same_status(client_num_locking, remote_num_locking); + + #[cfg(not(target_os = "windows"))] + let disable_numlock = false; + #[cfg(target_os = "windows")] + let disable_numlock = is_numlock_disabled(key_event); + + if need_click_capslock { + click_capslock(&mut en); + } + + if need_click_numlock && !disable_numlock { + click_numlock(&mut en); + } +} + +fn map_keyboard_mode(evt: &KeyEvent) { + // map mode(1): Send keycode according to the peer platform. + record_pressed_key(evt.chr() as u64 + KEY_CHAR_START, evt.down); + + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + + // Wayland + #[cfg(target_os = "linux")] + if !*IS_X11.lock().unwrap() { + let mut en = ENIGO.lock().unwrap(); + let code = evt.chr() as u16; + + if evt.down { + en.key_down(enigo::Key::Raw(code)).ok(); + } else { + en.key_up(enigo::Key::Raw(code)); + } + return; + } + + sim_rdev_rawkey(evt.chr(), evt.down); +} + +#[cfg(target_os = "macos")] +fn add_flags_to_enigo(en: &mut Enigo, key_event: &KeyEvent) { + // When long-pressed the command key, then press and release + // the Tab key, there should be CGEventFlagCommand in the flag. + en.reset_flag(); + for ck in key_event.modifiers.iter() { + if let Some(key) = KEY_MAP.get(&ck.value()) { + en.add_flag(key); + } + } +} + +fn get_control_key_value(key_event: &KeyEvent) -> i32 { + if let Some(key_event::Union::ControlKey(ck)) = key_event.union { + ck.value() + } else { + -1 + } +} + +fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { + let ck_value = get_control_key_value(key_event); + fix_modifiers(&key_event.modifiers[..], en, ck_value); +} + +#[cfg(target_os = "linux")] +fn is_altgr_pressed() -> bool { + KEYS_DOWN + .lock() + .unwrap() + .get(&(ControlKey::RAlt.value() as _)) + .is_some() +} + +#[cfg(not(target_os = "macos"))] +fn press_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec) { + for ref ck in key_event.modifiers.iter() { + if let Some(key) = control_key_value_to_key(ck.value()) { + if !is_pressed(&key, en) { + #[cfg(target_os = "linux")] + if key == Key::Alt && is_altgr_pressed() { + continue; + } + en.key_down(key.clone()).ok(); + to_release.push(key.clone()); + #[cfg(windows)] + modifier_sleep(); + } + } + } +} + +fn sync_modifiers(en: &mut Enigo, key_event: &KeyEvent, _to_release: &mut Vec) { + #[cfg(target_os = "macos")] + add_flags_to_enigo(en, key_event); + + if key_event.down { + release_unpressed_modifiers(en, key_event); + #[cfg(not(target_os = "macos"))] + press_modifiers(en, key_event, _to_release); + } +} + +fn process_control_key(en: &mut Enigo, ck: &EnumOrUnknown, down: bool) { + if let Some(key) = control_key_value_to_key(ck.value()) { + if down { + en.key_down(key).ok(); + } else { + en.key_up(key); + } + } +} + +#[inline] +fn need_to_uppercase(en: &mut Enigo) -> bool { + get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en) +} + +fn process_chr(en: &mut Enigo, chr: u32, down: bool) { + let key = char_value_to_key(chr); + + if down { + if en.key_down(key).is_ok() { + } else { + if let Ok(chr) = char::try_from(chr) { + let mut s = chr.to_string(); + if need_to_uppercase(en) { + s = s.to_uppercase(); + } + en.key_sequence(&s); + }; + } + } else { + en.key_up(key); + } +} + +fn process_unicode(en: &mut Enigo, chr: u32) { + if let Ok(chr) = char::try_from(chr) { + en.key_sequence(&chr.to_string()); + } +} + +fn process_seq(en: &mut Enigo, sequence: &str) { + en.key_sequence(&sequence); +} + +#[cfg(not(target_os = "macos"))] +fn release_keys(en: &mut Enigo, to_release: &Vec) { + for key in to_release { + en.key_up(key.clone()); + } +} + +fn record_pressed_key(record_key: u64, down: bool) { + let mut key_down = KEYS_DOWN.lock().unwrap(); + if down { + key_down.insert(record_key, Instant::now()); + } else { + key_down.remove(&record_key); + } +} + +fn is_function_key(ck: &EnumOrUnknown) -> bool { + let mut res = false; + if ck.value() == ControlKey::CtrlAltDel.value() { + // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. + std::thread::spawn(|| { + allow_err!(send_sas()); + }); + res = true; + } else if ck.value() == ControlKey::LockScreen.value() { + lock_screen_2(); + res = true; + } + return res; +} + +fn legacy_keyboard_mode(evt: &KeyEvent) { + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + let mut to_release: Vec = Vec::new(); + + let mut en = ENIGO.lock().unwrap(); + sync_modifiers(&mut en, &evt, &mut to_release); + + let down = evt.down; + match evt.union { + Some(key_event::Union::ControlKey(ck)) => { + if is_function_key(&ck) { + return; + } + let record_key = ck.value() as u64; + record_pressed_key(record_key, down); + process_control_key(&mut en, &ck, down) + } + Some(key_event::Union::Chr(chr)) => { + let record_key = chr as u64 + KEY_CHAR_START; + record_pressed_key(record_key, down); + process_chr(&mut en, chr, down) + } + Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr), + Some(key_event::Union::Seq(ref seq)) => process_seq(&mut en, seq), + _ => {} + } + + #[cfg(not(target_os = "macos"))] + release_keys(&mut en, &to_release); +} + +pub fn handle_key_(evt: &KeyEvent) { + if EXITING.load(Ordering::SeqCst) { + return; + } + + if evt.down { + sync_numlock_capslock_status(evt) + } + match evt.mode.unwrap() { + KeyboardMode::Map => { + map_keyboard_mode(evt); + } + KeyboardMode::Translate => { + legacy_keyboard_mode(evt); + } + _ => { + legacy_keyboard_mode(evt); + } + } +} + +#[tokio::main(flavor = "current_thread")] +async fn lock_screen_2() { + lock_screen().await; +} + +#[tokio::main(flavor = "current_thread")] +async fn send_sas() -> ResultType<()> { + let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?; + timeout(1000, stream.send(&crate::ipc::Data::SAS)).await??; + Ok(()) +} + lazy_static::lazy_static! { + static ref MODIFIER_MAP: HashMap = [ + (ControlKey::Alt, Key::Alt), + (ControlKey::RAlt, Key::RightAlt), + (ControlKey::Control, Key::Control), + (ControlKey::RControl, Key::RightControl), + (ControlKey::Shift, Key::Shift), + (ControlKey::RShift, Key::RightShift), + (ControlKey::Meta, Key::Meta), + (ControlKey::RWin, Key::RWin), + ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); static ref KEY_MAP: HashMap = [ (ControlKey::Alt, Key::Alt), @@ -733,347 +1203,3 @@ lazy_static::lazy_static! { (ControlKey::Delete, true), ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); } - -pub fn handle_key(evt: &KeyEvent) { - #[cfg(target_os = "macos")] - if !*IS_SERVER { - // having GUI, run main GUI thread, otherwise crash - let evt = evt.clone(); - QUEUE.exec_async(move || handle_key_(&evt)); - return; - } - #[cfg(windows)] - crate::portable_service::client::handle_key(evt); - #[cfg(not(windows))] - handle_key_(evt); -} - -fn rdev_key_down_or_up(key: RdevKey, down_or_up: bool) { - let event_type = match down_or_up { - true => EventType::KeyPress(key), - false => EventType::KeyRelease(key), - }; - let delay = std::time::Duration::from_millis(20); - match simulate(&event_type) { - Ok(()) => (), - Err(_simulate_error) => { - log::error!("Could not send {:?}", &event_type); - } - } - // Let ths OS catchup (at least MacOS) - std::thread::sleep(delay); -} - -fn rdev_key_click(key: RdevKey) { - rdev_key_down_or_up(key, true); - rdev_key_down_or_up(key, false); -} - -fn sync_status(evt: &KeyEvent) -> (bool, bool) { - /* todo! Shift+delete */ - let mut en = ENIGO.lock().unwrap(); - - // remote caps status - let caps_locking = evt - .modifiers - .iter() - .position(|&r| r == ControlKey::CapsLock.into()) - .is_some(); - // remote numpad status - let num_locking = evt - .modifiers - .iter() - .position(|&r| r == ControlKey::NumLock.into()) - .is_some(); - - let click_capslock = (caps_locking && !en.get_key_state(enigo::Key::CapsLock)) - || (!caps_locking && en.get_key_state(enigo::Key::CapsLock)); - let click_numlock = (num_locking && !en.get_key_state(enigo::Key::NumLock)) - || (!num_locking && en.get_key_state(enigo::Key::NumLock)); - #[cfg(windows)] - let click_numlock = { - let code = evt.chr(); - let key = rdev::get_win_key(code, 0); - match key { - RdevKey::Home - | RdevKey::UpArrow - | RdevKey::PageUp - | RdevKey::LeftArrow - | RdevKey::RightArrow - | RdevKey::End - | RdevKey::DownArrow - | RdevKey::PageDown - | RdevKey::Insert - | RdevKey::Delete => en.get_key_state(enigo::Key::NumLock), - _ => click_numlock, - } - }; - return (click_capslock, click_numlock); -} - -fn map_keyboard_mode(evt: &KeyEvent) { - // map mode(1): Send keycode according to the peer platform. - #[cfg(windows)] - crate::platform::windows::try_change_desktop(); - - let (click_capslock, click_numlock) = sync_status(evt); - - // Wayland - #[cfg(target_os = "linux")] - if !*IS_X11.lock().unwrap() { - let mut en = ENIGO.lock().unwrap(); - let code = evt.chr() as u16; - - #[cfg(not(target_os = "macos"))] - if click_capslock { - en.key_click(enigo::Key::CapsLock); - } - #[cfg(not(target_os = "macos"))] - if click_numlock { - en.key_click(enigo::Key::NumLock); - } - #[cfg(target_os = "macos")] - en.key_down(enigo::Key::CapsLock); - - if evt.down { - en.key_down(enigo::Key::Raw(code)).ok(); - } else { - en.key_up(enigo::Key::Raw(code)); - } - return; - } - - #[cfg(not(target_os = "macos"))] - if click_capslock { - rdev_key_click(RdevKey::CapsLock); - } - #[cfg(not(target_os = "macos"))] - if click_numlock { - rdev_key_click(RdevKey::NumLock); - } - #[cfg(target_os = "macos")] - if evt.down && click_capslock { - rdev_key_down_or_up(RdevKey::CapsLock, evt.down); - } - - rdev_key_down_or_up(RdevKey::Unknown(evt.chr()), evt.down); - return; -} - -fn legacy_keyboard_mode(evt: &KeyEvent) { - let (click_capslock, click_numlock) = sync_status(evt); - - #[cfg(windows)] - crate::platform::windows::try_change_desktop(); - let mut en = ENIGO.lock().unwrap(); - if click_capslock { - en.key_click(Key::CapsLock); - } - if click_numlock { - en.key_click(Key::NumLock); - } - // disable numlock if press home etc when numlock is on, - // because we will get numpad value (7,8,9 etc) if not - #[cfg(windows)] - let mut _disable_numlock = false; - #[cfg(target_os = "macos")] - en.reset_flag(); - // When long-pressed the command key, then press and release - // the Tab key, there should be CGEventFlagCommand in the flag. - #[cfg(target_os = "macos")] - for ck in evt.modifiers.iter() { - if let Some(key) = KEY_MAP.get(&ck.value()) { - en.add_flag(key); - } - } - #[cfg(not(target_os = "macos"))] - let mut to_release = Vec::new(); - - if evt.down { - let ck = if let Some(key_event::Union::ControlKey(ck)) = evt.union { - ck.value() - } else { - -1 - }; - fix_modifiers(&evt.modifiers[..], &mut en, ck); - for ref ck in evt.modifiers.iter() { - if let Some(key) = KEY_MAP.get(&ck.value()) { - #[cfg(target_os = "linux")] - if key == &Key::Alt && !get_modifier_state(key.clone(), &mut en) { - // for AltGr on Linux - if KEYS_DOWN - .lock() - .unwrap() - .get(&(ControlKey::RAlt.value() as _)) - .is_some() - { - continue; - } - } - #[cfg(not(target_os = "macos"))] - if !get_modifier_state(key.clone(), &mut en) { - en.key_down(key.clone()).ok(); - modifier_sleep(); - to_release.push(key); - } - } - } - } - - match evt.union { - Some(key_event::Union::ControlKey(ck)) => { - if let Some(key) = KEY_MAP.get(&ck.value()) { - #[cfg(windows)] - if let Some(_) = NUMPAD_KEY_MAP.get(&ck.value()) { - _disable_numlock = en.get_key_state(Key::NumLock); - if _disable_numlock { - en.key_down(Key::NumLock).ok(); - en.key_up(Key::NumLock); - } - } - if evt.down { - en.key_down(key.clone()).ok(); - KEYS_DOWN - .lock() - .unwrap() - .insert(ck.value() as _, Instant::now()); - } else { - en.key_up(key.clone()); - KEYS_DOWN.lock().unwrap().remove(&(ck.value() as _)); - } - } else if ck.value() == ControlKey::CtrlAltDel.value() { - // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. - std::thread::spawn(|| { - allow_err!(send_sas()); - }); - } else if ck.value() == ControlKey::LockScreen.value() { - lock_screen_2(); - } - } - Some(key_event::Union::Chr(chr)) => { - if evt.down { - if en.key_down(get_layout(chr)).is_ok() { - KEYS_DOWN - .lock() - .unwrap() - .insert(chr as u64 + KEY_CHAR_START, Instant::now()); - } else { - if let Ok(chr) = char::try_from(chr) { - let mut x = chr.to_string(); - if get_modifier_state(Key::Shift, &mut en) - || get_modifier_state(Key::CapsLock, &mut en) - { - x = x.to_uppercase(); - } - en.key_sequence(&x); - } - } - KEYS_DOWN - .lock() - .unwrap() - .insert(chr as u64 + KEY_CHAR_START, Instant::now()); - } else { - en.key_up(get_layout(chr)); - KEYS_DOWN - .lock() - .unwrap() - .remove(&(chr as u64 + KEY_CHAR_START)); - } - } - Some(key_event::Union::Unicode(chr)) => { - if let Ok(chr) = char::try_from(chr) { - en.key_sequence(&chr.to_string()); - } - } - Some(key_event::Union::Seq(ref seq)) => { - en.key_sequence(&seq); - } - _ => {} - } - #[cfg(not(target_os = "macos"))] - for key in to_release { - en.key_up(key.clone()); - } -} - -pub fn handle_key_(evt: &KeyEvent) { - if EXITING.load(Ordering::SeqCst) { - return; - } - - match evt.mode.unwrap() { - KeyboardMode::Legacy => { - legacy_keyboard_mode(evt); - } - KeyboardMode::Map => { - map_keyboard_mode(evt); - } - KeyboardMode::Translate => { - legacy_keyboard_mode(evt); - } - _ => { - legacy_keyboard_mode(evt); - } - } -} - -#[tokio::main(flavor = "current_thread")] -async fn lock_screen_2() { - lock_screen().await; -} - -#[tokio::main(flavor = "current_thread")] -async fn send_sas() -> ResultType<()> { - let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?; - timeout(1000, stream.send(&crate::ipc::Data::SAS)).await??; - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use rdev::{listen, Event, EventType, Key}; - use std::sync::mpsc; - - #[test] - fn test_handle_key() { - // listen - let (tx, rx) = mpsc::channel(); - std::thread::spawn(move || { - std::env::set_var("KEYBOARD_ONLY", "y"); - let func = move |event: Event| { - tx.send(event).ok(); - }; - if let Err(error) = listen(func) { - println!("Error: {:?}", error); - } - }); - // set key/char base on char - let mut evt = KeyEvent::new(); - evt.set_chr(66); - evt.mode = KeyboardMode::Legacy.into(); - - evt.modifiers.push(ControlKey::CapsLock.into()); - - // press - evt.down = true; - handle_key(&evt); - if let Ok(listen_evt) = rx.recv() { - assert_eq!(listen_evt.event_type, EventType::KeyPress(Key::Num1)) - } - // release - evt.down = false; - handle_key(&evt); - if let Ok(listen_evt) = rx.recv() { - assert_eq!(listen_evt.event_type, EventType::KeyRelease(Key::Num1)) - } - } - #[test] - fn test_get_key_state() { - let mut en = ENIGO.lock().unwrap(); - println!( - "[*] test_get_key_state: {:?}", - en.get_key_state(enigo::Key::NumLock) - ); - } -} diff --git a/src/server/uinput.rs b/src/server/uinput.rs index 78b22c562..a2e91e57b 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -4,7 +4,7 @@ use evdev::{ uinput::{VirtualDevice, VirtualDeviceBuilder}, AttributeSet, EventType, InputEvent, }; -use hbb_common::{allow_err, bail, log, tokio, ResultType}; +use hbb_common::{allow_err, bail, log, tokio::{self, runtime::Runtime}, ResultType}; static IPC_CONN_TIMEOUT: u64 = 1000; static IPC_REQUEST_TIMEOUT: u64 = 1000; @@ -17,24 +17,24 @@ pub mod client { pub struct UInputKeyboard { conn: Connection, + rt: Runtime, } impl UInputKeyboard { pub async fn new() -> ResultType { let conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_KEYBOARD).await?; - Ok(Self { conn }) + let rt = Runtime::new()?; + Ok(Self { conn, rt }) } - #[tokio::main(flavor = "current_thread")] - async fn send(&mut self, data: Data) -> ResultType<()> { - self.conn.send(&data).await + fn send(&mut self, data: Data) -> ResultType<()> { + self.rt.block_on(self.conn.send(&data)) } - #[tokio::main(flavor = "current_thread")] - async fn send_get_key_state(&mut self, data: Data) -> ResultType { - self.conn.send(&data).await?; + fn send_get_key_state(&mut self, data: Data) -> ResultType { + self.rt.block_on(self.conn.send(&data))?; - match self.conn.next_timeout(IPC_REQUEST_TIMEOUT).await { + match self.rt.block_on(self.conn.next_timeout(IPC_REQUEST_TIMEOUT)) { Ok(Some(Data::KeyboardResponse(ipc::DataKeyboardResponse::GetKeyState(state)))) => { Ok(state) } @@ -101,17 +101,18 @@ pub mod client { pub struct UInputMouse { conn: Connection, + rt: Runtime, } impl UInputMouse { pub async fn new() -> ResultType { let conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_MOUSE).await?; - Ok(Self { conn }) + let rt = Runtime::new()?; + Ok(Self { conn, rt }) } - #[tokio::main(flavor = "current_thread")] - async fn send(&mut self, data: Data) -> ResultType<()> { - self.conn.send(&data).await + fn send(&mut self, data: Data) -> ResultType<()> { + self.rt.block_on(self.conn.send(&data)) } pub fn send_refresh(&mut self) -> ResultType<()> { @@ -586,6 +587,16 @@ pub mod service { match data { Data::Mouse(data) => { if let DataMouse::Refresh = data { + let resolution = RESOLUTION.lock().unwrap(); + let rng_x = resolution.0.clone(); + let rng_y = resolution.1.clone(); + log::info!( + "Refresh uinput mouce with rng_x: ({}, {}), rng_y: ({}, {})", + rng_x.0, + rng_x.1, + rng_y.0, + rng_y.1 + ); mouse = match mouce::Mouse::new_uinput(rng_x, rng_y) { Ok(mouse) => mouse, Err(e) => { diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs index d75596157..47bf49707 100644 --- a/src/server/video_qos.rs +++ b/src/server/video_qos.rs @@ -198,7 +198,7 @@ impl VideoQoS { #[cfg(target_os = "android")] { - // fix when andorid screen shrinks + // fix when android screen shrinks let fix = scrap::Display::fix_quality() as u32; log::debug!("Android screen, fix quality:{}", fix); let base_bitrate = base_bitrate * fix; diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 6d1235ed8..b986c785c 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -78,11 +78,11 @@ fn is_capturer_mag_supported() -> bool { false } -pub fn capture_cursor_embeded() -> bool { - scrap::is_cursor_embeded() +pub fn capture_cursor_embedded() -> bool { + scrap::is_cursor_embedded() } -pub fn notify_video_frame_feched(conn_id: i32, frame_tm: Option) { +pub fn notify_video_frame_fetched(conn_id: i32, frame_tm: Option) { FRAME_FETCHED_NOTIFIER.0.send((conn_id, frame_tm)).unwrap() } @@ -146,7 +146,7 @@ impl VideoFrameController { fetched_conn_ids.insert(id); } Ok(None) => { - // this branch would nerver be reached + // this branch would never be reached } } } @@ -162,7 +162,7 @@ fn check_display_changed( last_n: usize, last_current: usize, last_width: usize, - last_hegiht: usize, + last_height: usize, ) -> bool { #[cfg(target_os = "linux")] { @@ -187,7 +187,7 @@ fn check_display_changed( if i != last_current { return true; }; - if d.width() != last_width || d.height() != last_hegiht { + if d.width() != last_width || d.height() != last_height { return true; }; } @@ -249,7 +249,7 @@ fn create_capturer( PRIVACY_WINDOW_NAME ); } - log::debug!("Create maginifier capture for {}", privacy_mode_id); + log::debug!("Create magnifier capture for {}", privacy_mode_id); c = Some(Box::new(c1)); } Err(e) => { @@ -385,10 +385,12 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType ResultType<()> { #[cfg(windows)] ensure_close_virtual_device()?; - // ensure_inited() is needed because release_resouce() may be called. + // ensure_inited() is needed because release_resource() may be called. #[cfg(target_os = "linux")] super::wayland::ensure_inited()?; #[cfg(windows)] @@ -464,7 +466,7 @@ fn run(sp: GenericService) -> ResultType<()> { y: c.origin.1 as _, width: c.width as _, height: c.height as _, - cursor_embeded: capture_cursor_embeded(), + cursor_embedded: capture_cursor_embedded(), ..Default::default() }); let mut msg_out = Message::new(); @@ -599,7 +601,7 @@ fn run(sp: GenericService) -> ResultType<()> { would_block_count += 1; if !scrap::is_x11() { if would_block_count >= 100 { - super::wayland::release_resouce(); + super::wayland::release_resource(); bail!("Wayland capturer none 100 times, try restart captuere"); } } @@ -653,7 +655,7 @@ fn run(sp: GenericService) -> ResultType<()> { #[cfg(target_os = "linux")] if !scrap::is_x11() { - super::wayland::release_resouce(); + super::wayland::release_resource(); } Ok(()) @@ -821,7 +823,7 @@ pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { height: d.height() as _, name: d.name(), online: d.is_online(), - cursor_embeded: false, + cursor_embedded: false, ..Default::default() }); } diff --git a/src/server/wayland.rs b/src/server/wayland.rs index fdf9bccec..24b3be110 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -129,7 +129,7 @@ pub(super) async fn check_init() -> ResultType<()> { let num = all.len(); let (primary, mut displays) = super::video_service::get_displays_2(&all); for display in displays.iter_mut() { - display.cursor_embeded = true; + display.cursor_embedded = true; } let mut rects: Vec<((i32, i32), usize, usize)> = Vec::new(); @@ -244,7 +244,7 @@ pub(super) fn get_display_num() -> ResultType { } #[allow(dead_code)] -pub(super) fn release_resouce() { +pub(super) fn release_resource() { if scrap::is_x11() { return; } diff --git a/src/tray.rs b/src/tray.rs index 3658739a4..98a4127a3 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,3 +1,4 @@ +#[cfg(any(target_os = "linux", target_os = "windows"))] use super::ui_interface::get_option_opt; #[cfg(target_os = "linux")] use hbb_common::log::{debug, error, info}; @@ -44,7 +45,7 @@ pub fn start_tray() { } else { *control_flow = ControlFlow::Wait; } - let stopped = is_service_stoped(); + let stopped = is_service_stopped(); let state = if stopped { 2 } else { 1 }; let old = *old_state.lock().unwrap(); if state != old { @@ -88,6 +89,9 @@ pub fn start_tray() { /// This function will block current execution, show the tray icon and handle events. #[cfg(target_os = "linux")] pub fn start_tray() { + use std::time::Duration; + + use glib::{clone, Continue}; use gtk::traits::{GtkMenuItemExt, MenuShellExt, WidgetExt}; info!("configuring tray"); @@ -98,7 +102,7 @@ pub fn start_tray() { } if let Some(mut appindicator) = get_default_app_indicator() { let mut menu = gtk::Menu::new(); - let stoped = is_service_stoped(); + let stoped = is_service_stopped(); // start/stop service let label = if stoped { crate::client::translate("Start Service".to_owned()) @@ -106,9 +110,9 @@ pub fn start_tray() { crate::client::translate("Stop service".to_owned()) }; let menu_item_service = gtk::MenuItem::with_label(label.as_str()); - menu_item_service.connect_activate(move |item| { + menu_item_service.connect_activate(move |_| { let _lock = crate::ui_interface::SENDER.lock().unwrap(); - update_tray_service_item(item); + change_service_state(); }); menu.append(&menu_item_service); // show tray item @@ -116,6 +120,16 @@ pub fn start_tray() { appindicator.set_menu(&mut menu); // start event loop info!("Setting tray event loop"); + // check the connection status for every second + glib::timeout_add_local( + Duration::from_secs(1), + clone!(@strong menu_item_service as item => move || { + let _lock = crate::ui_interface::SENDER.lock().unwrap(); + update_tray_service_item(&item); + // continue to trigger the next status check + Continue(true) + }), + ); gtk::main(); } else { error!("Tray process exit now"); @@ -123,20 +137,28 @@ pub fn start_tray() { } #[cfg(target_os = "linux")] -fn update_tray_service_item(item: >k::MenuItem) { - use gtk::traits::GtkMenuItemExt; - - if is_service_stoped() { +fn change_service_state() { + if is_service_stopped() { debug!("Now try to start service"); - item.set_label(&crate::client::translate("Stop service".to_owned())); crate::ipc::set_option("stop-service", ""); } else { debug!("Now try to stop service"); - item.set_label(&crate::client::translate("Start Service".to_owned())); crate::ipc::set_option("stop-service", "Y"); } } +#[cfg(target_os = "linux")] +#[inline] +fn update_tray_service_item(item: >k::MenuItem) { + use gtk::traits::GtkMenuItemExt; + + if is_service_stopped() { + item.set_label(&crate::client::translate("Start Service".to_owned())); + } else { + item.set_label(&crate::client::translate("Stop service".to_owned())); + } +} + #[cfg(target_os = "linux")] fn get_default_app_indicator() -> Option { use libappindicator::AppIndicatorStatus; @@ -173,7 +195,8 @@ fn get_default_app_indicator() -> Option { /// Check if service is stoped. /// Return [`true`] if service is stoped, [`false`] otherwise. #[inline] -fn is_service_stoped() -> bool { +#[cfg(any(target_os = "linux", target_os = "windows"))] +fn is_service_stopped() -> bool { if let Some(v) = get_option_opt("stop-service") { v == "Y" } else { @@ -185,14 +208,14 @@ fn is_service_stoped() -> bool { pub fn make_tray() { use tray_item::TrayItem; let mode = dark_light::detect(); - let mut icon_path = ""; + let icon_path; match mode { dark_light::Mode::Dark => { icon_path = "mac-tray-light.png"; - }, + } dark_light::Mode::Light => { icon_path = "mac-tray-dark.png"; - }, + } } if let Ok(mut tray) = TrayItem::new(&crate::get_app_name(), icon_path) { tray.add_label(&format!( @@ -211,4 +234,3 @@ pub fn make_tray() { } } } - diff --git a/src/ui.rs b/src/ui.rs index e282d19c4..d45a64298 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,13 +9,8 @@ use sciter::Value; use hbb_common::{ allow_err, - config::{self, Config, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, - futures::future::join_all, + config::{self, PeerConfig}, log, - protobuf::Message as _, - rendezvous_proto::*, - tcp::FramedStream, - tokio, }; use crate::common::get_app_name; @@ -31,8 +26,6 @@ pub mod remote; #[cfg(target_os = "windows")] pub mod win_privacy; -type Message = RendezvousMessage; - pub type Children = Arc)>>; #[allow(dead_code)] type Status = (i32, bool, i64, String); @@ -124,12 +117,12 @@ pub fn start(args: &mut [String]) { let args: Vec = iter.map(|x| x.clone()).collect(); frame.set_title(&id); frame.register_behavior("native-remote", move || { - Box::new(remote::SciterSession::new( - cmd.clone(), - id.clone(), - pass.clone(), - args.clone(), - )) + let handler = + remote::SciterSession::new(cmd.clone(), id.clone(), pass.clone(), args.clone()); + #[cfg(not(feature = "flutter"))] + crate::keyboard::set_cur_session(handler.inner()); + + Box::new(handler) }); page = "remote.html"; } else { @@ -510,7 +503,7 @@ impl UI { fn change_id(&self, id: String) { let old_id = self.get_id(); - change_id(id, old_id); + change_id_shared(id, old_id); } fn post_request(&self, url: String, body: String, header: String) { @@ -690,101 +683,6 @@ fn get_sound_inputs() -> Vec { .collect() } -const INVALID_FORMAT: &'static str = "Invalid format"; -const UNKNOWN_ERROR: &'static str = "Unknown error"; - -#[tokio::main(flavor = "current_thread")] -async fn change_id(id: String, old_id: String) -> &'static str { - if !hbb_common::is_valid_custom_id(&id) { - return INVALID_FORMAT; - } - let uuid = machine_uid::get().unwrap_or("".to_owned()); - if uuid.is_empty() { - return UNKNOWN_ERROR; - } - let rendezvous_servers = crate::ipc::get_rendezvous_servers(1_000).await; - let mut futs = Vec::new(); - let err: Arc> = Default::default(); - for rendezvous_server in rendezvous_servers { - let err = err.clone(); - let id = id.to_owned(); - let uuid = uuid.clone(); - let old_id = old_id.clone(); - futs.push(tokio::spawn(async move { - let tmp = check_id(rendezvous_server, old_id, id, uuid).await; - if !tmp.is_empty() { - *err.lock().unwrap() = tmp; - } - })); - } - join_all(futs).await; - let err = *err.lock().unwrap(); - if err.is_empty() { - crate::ipc::set_config_async("id", id.to_owned()).await.ok(); - } - err -} - -async fn check_id( - rendezvous_server: String, - old_id: String, - id: String, - uuid: String, -) -> &'static str { - let any_addr = Config::get_any_listen_addr(); - if let Ok(mut socket) = FramedStream::new( - crate::check_port(rendezvous_server, RENDEZVOUS_PORT), - any_addr, - RENDEZVOUS_TIMEOUT, - ) - .await - { - let mut msg_out = Message::new(); - msg_out.set_register_pk(RegisterPk { - old_id, - id, - uuid: uuid.into(), - ..Default::default() - }); - let mut ok = false; - if socket.send(&msg_out).await.is_ok() { - if let Some(Ok(bytes)) = socket.next_timeout(3_000).await { - if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { - match msg_in.union { - Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => { - match rpr.result.enum_value_or_default() { - register_pk_response::Result::OK => { - ok = true; - } - register_pk_response::Result::ID_EXISTS => { - return "Not available"; - } - register_pk_response::Result::TOO_FREQUENT => { - return "Too frequent"; - } - register_pk_response::Result::NOT_SUPPORT => { - return "server_not_support"; - } - register_pk_response::Result::INVALID_ID_FORMAT => { - return INVALID_FORMAT; - } - _ => {} - } - } - _ => {} - } - } - } - } - if !ok { - return UNKNOWN_ERROR; - } - } else { - return "Failed to connect to rendezvous server"; - } - "" -} - // sacrifice some memory pub fn value_crash_workaround(values: &[Value]) -> Arc> { let persist = Arc::new(values.to_vec()); diff --git a/src/ui/cm.css b/src/ui/cm.css index ff4d422e4..960c8b567 100644 --- a/src/ui/cm.css +++ b/src/ui/cm.css @@ -45,7 +45,7 @@ div.right-panel { div.icon-and-id { flow: horizontal; - border-spacing: 1em; + border-spacing: 1em; } div.icon { @@ -64,7 +64,7 @@ div.id { div.permissions { flow: horizontal; - border-spacing: 0.5em; + border-spacing: 0.5em; } div.permissions > div { @@ -141,7 +141,7 @@ button.elevate>span { } button.elevate>span>span { - margin-left:*; + margin-left:*; margin-right:*; } diff --git a/src/ui/cm.html b/src/ui/cm.html index 4edb4a762..aabaa0294 100644 --- a/src/ui/cm.html +++ b/src/ui/cm.html @@ -4,7 +4,7 @@ @import url(common.css); @import url(cm.css); - diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 74eb6c6d2..716f2c6dd 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -31,7 +31,7 @@ class Body: Reactor.Component var disconnected = c.disconnected; var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && c.port_forward.length == 0; var show_accept_btn = handler.get_option('approve-mode') != 'password'; - // below size:* is work around for Linux, it alreayd set in css, but not work, shit sciter + // below size:* is work around for Linux, it already set in css, but not work, shit sciter return
    diff --git a/src/ui/common.css b/src/ui/common.css index 1814ad32d..0fb9afcb1 100644 --- a/src/ui/common.css +++ b/src/ui/common.css @@ -56,7 +56,7 @@ button[type=checkbox], button[type=checkbox]:active { button.outline { border: color(border) solid 1px; - background: transparent; + background: transparent; color: color(text); } @@ -115,7 +115,7 @@ textarea:empty { .base:disabled { background: transparent; } .slider:hover { background: grey; } .slider:active { background: grey; } - .base { size: 16px; } + .base { size: 16px; } .corner { background: white; } } @@ -185,7 +185,7 @@ header div.window-icon icon { header caption { size: *; -} +} @media platform != "OSX" { button.window { diff --git a/src/ui/file_transfer.css b/src/ui/file_transfer.css index 9b45ea2b7..7fd4ac7a8 100644 --- a/src/ui/file_transfer.css +++ b/src/ui/file_transfer.css @@ -12,22 +12,22 @@ div#file-transfer { } table -{ +{ font: system; border: 1px solid color(border); flow: table-fixed; prototype: Grid; size: *; padding:0; - border-spacing: 0; + border-spacing: 0; overflow-x: auto; overflow-y: hidden; } - -table > thead { + +table > thead { behavior: column-resizer; border-bottom: color(border) solid 1px; -} +} table > tbody { behavior: select-multiple; @@ -41,20 +41,20 @@ table th { } table th -{ +{ padding: 4px; foreground-repeat: no-repeat; foreground-position: 50% 3px auto auto; border-left: color(border) solid 1px; -} +} -table th.sortable[sort=asc] -{ +table th.sortable[sort=asc] +{ foreground-image: url(stock:arrow-down); -} +} table th.sortable[sort=desc] -{ +{ foreground-image: url(stock:arrow-up); } @@ -81,10 +81,10 @@ table.has_current thead th:current { table tr:nth-child(odd) { background-color: white; } /* each odd row */ table tr:nth-child(even) { background-color: #F4F5F6; } /* each even row */ -table.has_current tr:current /* current row */ -{ - background-color: color(accent); -} +table.has_current tr:current /* current row */ +{ + background-color: color(accent); +} table.has_current tbody tr:checked { @@ -95,9 +95,9 @@ table.has_current tbody tr:checked td { color: highlighttext; } -table td -{ - padding: 4px; +table td +{ + padding: 4px; text-align: left; font-size: 1em; height: 1.4em; @@ -124,11 +124,11 @@ table td:nth-child(4) { section { size: *; margin: 1em; - border-spacing: 0.5em; + border-spacing: 0.5em; } table td:nth-child(1) { - foreground-repeat: no-repeat; + foreground-repeat: no-repeat; foreground-position: 50% 50% } @@ -160,11 +160,11 @@ div.toolbar > div.button:hover { div.toolbar > div.send { flow: horizontal; - border-spacing: 0.5em; + border-spacing: 0.5em; } div.remote > div.send svg { - transform: scale(-1, 1); + transform: scale(-1, 1); } div.navbar { @@ -207,7 +207,7 @@ table.job-table tr td { padding: 0.5em 1em; border-bottom: color(border) 1px solid; flow: horizontal; - border-spacing: 1em; + border-spacing: 1em; height: 3em; overflow-x: hidden; } @@ -217,11 +217,11 @@ table.job-table tr svg { } table.job-table tr.is_remote svg { - transform: scale(-1, 1); + transform: scale(-1, 1); } table.job-table tr.is_remote div.svg_continue svg { - transform: scale(1, 1); + transform: scale(1, 1); } table.job-table tr td div.text { @@ -246,7 +246,7 @@ table#port-forward thead tr th { table#port-forward tr td { height: 3em; - text-align: left; + text-align: left; } table#port-forward input[type=text], table#port-forward input[type=number] { diff --git a/src/ui/header.css b/src/ui/header.css index e248b46d5..8fe408612 100644 --- a/src/ui/header.css +++ b/src/ui/header.css @@ -8,7 +8,7 @@ header #screens { height: 22px; border-radius: 4px; flow: horizontal; - border-spacing: 0.5em; + border-spacing: 0.5em; padding-right: 1em; position: relative; } diff --git a/src/ui/header.tis b/src/ui/header.tis index 086696726..dd0b35541 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -164,10 +164,10 @@ class Header: Reactor.Component { var codecs = handler.supported_hwcodec(); var show_codec = handler.has_hwcodec() && (codecs[0] || codecs[1]); - var cursor_embeded = false; + var cursor_embedded = false; if ((pi.displays || []).length > 0) { if (pi.displays.length > pi.current_display) { - cursor_embeded = pi.displays[pi.current_display].cursor_embeded; + cursor_embedded = pi.displays[pi.current_display].cursor_embedded; } } @@ -191,7 +191,7 @@ class Header: Reactor.Component { {codecs[1] ?
  • {svg_checkmark}H265
  • : ""}
    : ""}
    - {!cursor_embeded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • } + {!cursor_embedded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • }
  • {svg_checkmark}{translate('Show quality monitor')}
  • {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} {is_win && pi.platform == 'Windows' && file_enabled ?
  • {svg_checkmark}{translate('Allow file copy and paste')}
  • : ""} @@ -208,7 +208,7 @@ class Header: Reactor.Component { {keyboard_enabled ?
  • {translate('OS Password')}
  • : ""}
  • {translate('Transfer File')}
  • {translate('TCP Tunneling')}
  • - {handler.get_audit_server() &&
  • {translate('Note')}
  • } + {handler.get_audit_server("conn") &&
  • {translate('Note')}
  • }
    {keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ?
  • {translate('Insert')} Ctrl + Alt + Del
  • : ""} {restart_enabled && (pi.platform == "Linux" || pi.platform == "Windows" || pi.platform == "Mac OS") ?
  • {translate('Restart Remote Device')}
  • : ""} diff --git a/src/ui/index.tis b/src/ui/index.tis index 9dcd4f4c4..c141d0efe 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -361,7 +361,7 @@ class MyIdMenu: Reactor.Component { function showAbout() { var name = handler.get_app_name(); - msgbox("custom-nocancel-nook-hasclose", "About " + name, "
    \ + msgbox("custom-nocancel-nook-hasclose", translate("About") + " " + name, "
    \
    Version: " + handler.get_version() + " \
    Privacy Statement
    \
    Website
    \ diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 488d1afc8..ab3fb9079 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -13,7 +13,6 @@ use objc::{ }; use sciter::{make_args, Host}; use std::{ffi::c_void, rc::Rc}; -use dark_light; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 3d209a71c..1f3d5f7ec 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -79,8 +79,8 @@ impl InvokeUiSession for SciterHandler { } } - fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embeded: bool) { - self.call("setDisplay", &make_args!(x, y, w, h, cursor_embeded)); + fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool) { + self.call("setDisplay", &make_args!(x, y, w, h, cursor_embedded)); // https://sciter.com/forums/topic/color_spaceiyuv-crash // Nothing spectacular in decoder – done on CPU side. // So if you can do BGRA translation on your side – the better. @@ -223,7 +223,7 @@ impl InvokeUiSession for SciterHandler { display.set_item("y", d.y); display.set_item("width", d.width); display.set_item("height", d.height); - display.set_item("cursor_embeded", d.cursor_embeded); + display.set_item("cursor_embedded", d.cursor_embedded); displays.push(display); } pi_sciter.set_item("displays", displays); @@ -231,6 +231,17 @@ impl InvokeUiSession for SciterHandler { self.call("updatePi", &make_args!(pi_sciter)); } + fn on_connected(&self, conn_type: ConnType) { + match conn_type { + ConnType::RDP => {} + ConnType::PORT_FORWARD => {} + ConnType::FILE_TRANSFER => {} + ConnType::DEFAULT_CONN => { + crate::keyboard::client::start_grab_loop(); + } + } + } + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { self.call2( "msgbox_retry", @@ -337,7 +348,7 @@ impl sciter::EventHandler for SciterSession { } sciter::dispatch_script_call! { - fn get_audit_server(); + fn get_audit_server(String); fn send_note(String); fn is_xfce(); fn get_id(); @@ -434,6 +445,10 @@ impl SciterSession { Self(session) } + pub fn inner(&self) -> Session { + self.0.clone() + } + fn get_custom_image_quality(&mut self) -> Value { let mut v = Value::array(0); for x in self.lc.read().unwrap().custom_image_quality.iter() { diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 012205abc..63df0cb09 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -6,7 +6,7 @@ var display_width = 0; var display_height = 0; var display_origin_x = 0; var display_origin_y = 0; -var display_cursor_embeded = false; +var display_cursor_embedded = false; var display_scale = 1; var keyboard_enabled = true; // server side var clipboard_enabled = true; // server side @@ -16,17 +16,17 @@ var restart_enabled = true; // server side var recording_enabled = true; // server side var scroll_body = $(body); -handler.setDisplay = function(x, y, w, h, cursor_embeded) { +handler.setDisplay = function(x, y, w, h, cursor_embedded) { display_width = w; display_height = h; display_origin_x = x; display_origin_y = y; - display_cursor_embeded = cursor_embeded; + display_cursor_embedded = cursor_embedded; adaptDisplay(); if (recording) handler.record_screen(true, w, h); } -// in case toolbar not shown correclty +// in case toolbar not shown correctly view.windowMinSize = (scaleIt(500), scaleIt(300)); function adaptDisplay() { @@ -197,7 +197,7 @@ function handler.onMouse(evt) dragging = false; break; case Event.MOUSE_MOVE: - if (display_cursor_embeded) { + if (display_cursor_embedded) { break; } if (cursor_img.style#display != "none" && keyboard_enabled) { @@ -365,7 +365,7 @@ function updateCursor(system=false) { } function refreshCursor() { - if (display_cursor_embeded) { + if (display_cursor_embedded) { cursor_img.style#display = "none"; return; } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 604d2e222..3b7d1c2c0 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -14,20 +14,17 @@ use hbb_common::{ tokio::{self, sync::mpsc, time}, }; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] use hbb_common::{ config::{RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, futures::future::join_all, protobuf::Message as _, rendezvous_proto::*, - tcp::FramedStream, }; #[cfg(feature = "flutter")] use crate::hbbs_http::account; use crate::{common::SOFTWARE_UPDATE_URL, ipc}; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] type Message = RendezvousMessage; pub type Children = Arc)>>; @@ -161,6 +158,7 @@ pub fn get_license() -> String { } #[inline] +#[cfg(any(target_os = "linux", target_os = "windows"))] pub fn get_option_opt(key: &str) -> Option { OPTIONS.lock().unwrap().get(key).map(|x| x.clone()) } @@ -202,6 +200,18 @@ pub fn set_local_flutter_config(key: String, value: String) { LocalConfig::set_flutter_config(key, value); } +#[cfg(feature = "flutter")] +#[inline] +pub fn get_kb_layout_type() -> String { + LocalConfig::get_kb_layout_type() +} + +#[cfg(feature = "flutter")] +#[inline] +pub fn set_kb_layout_type(kb_layout_type: String) { + LocalConfig::set_kb_layout_type(kb_layout_type); +} + #[inline] pub fn peer_has_password(id: String) -> bool { !PeerConfig::load(&id).password.is_empty() @@ -572,6 +582,14 @@ pub fn is_installed_daemon(_prompt: bool) -> bool { return true; } +#[inline] +pub fn is_can_input_monitoring(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_can_input_monitoring(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + #[inline] pub fn get_error() -> String { #[cfg(not(any(feature = "cli")))] @@ -742,7 +760,7 @@ pub fn change_id(id: String) { *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); let old_id = get_id(); std::thread::spawn(move || { - *ASYNC_JOB_STATUS.lock().unwrap() = change_id_(id, old_id).to_owned(); + *ASYNC_JOB_STATUS.lock().unwrap() = change_id_shared(id, old_id).to_owned(); }); } @@ -999,14 +1017,11 @@ pub(crate) async fn send_to_cm(data: &ipc::Data) { } } -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] const INVALID_FORMAT: &'static str = "Invalid format"; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] const UNKNOWN_ERROR: &'static str = "Unknown error"; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[tokio::main(flavor = "current_thread")] -async fn change_id_(id: String, old_id: String) -> &'static str { +pub async fn change_id_shared(id: String, old_id: String) -> &'static str { if !hbb_common::is_valid_custom_id(&id) { return INVALID_FORMAT; } @@ -1054,17 +1069,14 @@ async fn change_id_(id: String, old_id: String) -> &'static str { err } -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] async fn check_id( rendezvous_server: String, old_id: String, id: String, uuid: String, ) -> &'static str { - let any_addr = Config::get_any_listen_addr(); - if let Ok(mut socket) = FramedStream::new( + if let Ok(mut socket) = hbb_common::socket_client::connect_tcp( crate::check_port(rendezvous_server, RENDEZVOUS_PORT), - any_addr, RENDEZVOUS_TIMEOUT, ) .await diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 6b635436d..c66e1fa3b 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,13 +1,11 @@ use crate::client::io_loop::Remote; use crate::client::{ - check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, - load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, - QualityStatus, KEY_MAP, + check_if_retry, handle_hash, handle_login_error, handle_login_from_ui, handle_test_delay, + input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, + LoginConfigHandler, QualityStatus, KEY_MAP, }; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::client::{get_key_state, SERVER_KEYBOARD_ENABLED}; -#[cfg(target_os = "linux")] -use crate::common::IS_X11; +use crate::common::GrabState; +use crate::keyboard; use crate::{client::Data, client::Interface}; use async_trait::async_trait; use hbb_common::config::{Config, LocalConfig, PeerConfig}; @@ -15,53 +13,12 @@ use hbb_common::rendezvous_proto::ConnType; use hbb_common::tokio::{self, sync::mpsc}; use hbb_common::{allow_err, message_proto::*}; use hbb_common::{fs, get_version_number, log, Stream}; -use rdev::{Event, EventType, EventType::*, Key as RdevKey}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use rdev::{Keyboard as RdevKeyboard, KeyboardState}; -use std::collections::{HashMap, HashSet}; +use rdev::{Event, EventType::*}; +use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; - -/// IS_IN KEYBOARD_HOOKED sciter only pub static IS_IN: AtomicBool = AtomicBool::new(false); -pub static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); -pub static HOTKEY_HOOKED: AtomicBool = AtomicBool::new(false); -#[cfg(windows)] -static mut IS_ALT_GR: bool = false; -#[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::flutter::FlutterHandler; - -lazy_static::lazy_static! { - static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -lazy_static::lazy_static! { - static ref KEYBOARD: Arc> = Arc::new(Mutex::new(RdevKeyboard::new().unwrap())); -} - -#[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -lazy_static::lazy_static! { - pub static ref CUR_SESSION: Arc>>> = Default::default(); -} - -lazy_static::lazy_static! { - static ref MUTEX_SPECIAL_KEYS: Mutex> = { - let mut m = HashMap::new(); - m.insert(RdevKey::ShiftLeft, false); - m.insert(RdevKey::ShiftRight, false); - m.insert(RdevKey::ControlLeft, false); - m.insert(RdevKey::ControlRight, false); - m.insert(RdevKey::Alt, false); - m.insert(RdevKey::AltGr, false); - m.insert(RdevKey::MetaLeft, false); - m.insert(RdevKey::MetaRight, false); - Mutex::new(m) - }; -} #[derive(Clone, Default)] pub struct Session { @@ -75,6 +32,32 @@ pub struct Session { } impl Session { + pub fn is_file_transfer(&self) -> bool { + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::FILE_TRANSFER) + } + + pub fn is_port_forward(&self) -> bool { + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::PORT_FORWARD) + } + + pub fn is_rdp(&self) -> bool { + self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) + } + + pub fn set_connection_info(&mut self, direct: bool, received: bool) { + let mut lc = self.lc.write().unwrap(); + lc.direct = Some(direct); + lc.received = received; + } + pub fn get_view_style(&self) -> String { self.lc.read().unwrap().view_style.clone() } @@ -92,11 +75,11 @@ impl Session { } pub fn get_keyboard_mode(&self) -> String { - global_get_keyboard_mode() + self.lc.read().unwrap().keyboard_mode.clone() } - pub fn save_keyboard_mode(&self, value: String) { - global_save_keyboard_mode(value); + pub fn save_keyboard_mode(&mut self, value: String) { + self.lc.write().unwrap().save_keyboard_mode(value); } pub fn save_view_style(&mut self, value: String) { @@ -216,7 +199,7 @@ impl Session { self.send(Data::Message(msg)); } - pub fn get_audit_server(&self) -> String { + pub fn get_audit_server(&self, typ: String) -> String { if self.lc.read().unwrap().conn_id <= 0 || LocalConfig::get_option("access_token").is_empty() { @@ -225,11 +208,12 @@ impl Session { crate::get_audit_server( Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"), + typ, ) } pub fn send_note(&self, note: String) { - let url = self.get_audit_server(); + let url = self.get_audit_server("conn".to_string()); let id = self.id.clone(); let conn_id = self.lc.read().unwrap().conn_id; std::thread::spawn(move || { @@ -307,439 +291,6 @@ impl Session { self.lc.read().unwrap().info.platform.clone() } - pub fn ctrl_alt_del(&self) { - if self.peer_platform() == "Windows" { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::CtrlAltDel); - // todo - key_event.down = true; - self.send_key_event(key_event, KeyboardMode::Legacy); - } else { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::Delete); - self.legacy_modifiers(&mut key_event, true, true, false, false); - // todo - key_event.press = true; - self.send_key_event(key_event, KeyboardMode::Legacy); - } - } - - fn send_key_event(&self, mut evt: KeyEvent, keyboard_mode: KeyboardMode) { - // mode: legacy(0), map(1), translate(2), auto(3) - evt.mode = keyboard_mode.into(); - let mut msg_out = Message::new(); - msg_out.set_key_event(evt); - self.send(Data::Message(msg_out)); - } - - #[allow(dead_code)] - fn convert_numpad_keys(&self, key: RdevKey) -> RdevKey { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if get_key_state(enigo::Key::NumLock) { - return key; - } - match key { - RdevKey::Kp0 => RdevKey::Insert, - RdevKey::KpDecimal => RdevKey::Delete, - RdevKey::Kp1 => RdevKey::End, - RdevKey::Kp2 => RdevKey::DownArrow, - RdevKey::Kp3 => RdevKey::PageDown, - RdevKey::Kp4 => RdevKey::LeftArrow, - RdevKey::Kp5 => RdevKey::Clear, - RdevKey::Kp6 => RdevKey::RightArrow, - RdevKey::Kp7 => RdevKey::Home, - RdevKey::Kp8 => RdevKey::UpArrow, - RdevKey::Kp9 => RdevKey::PageUp, - _ => key, - } - } - - fn map_keyboard_mode(&self, down_or_up: bool, key: RdevKey, _evt: Option) { - // map mode(1): Send keycode according to the peer platform. - #[cfg(target_os = "windows")] - let key = if let Some(e) = _evt { - rdev::get_win_key(e.code.into(), e.scan_code) - } else { - key - }; - - let peer = self.peer_platform(); - let mut key_event = KeyEvent::new(); - // According to peer platform. - let keycode: u32 = if peer == "Linux" { - rdev::linux_keycode_from_key(key).unwrap_or_default().into() - } else if peer == "Windows" { - rdev::win_keycode_from_key(key).unwrap_or_default().into() - } else { - // Without Clear Key on Mac OS - if key == rdev::Key::Clear { - return; - } - rdev::macos_keycode_from_key(key).unwrap_or_default().into() - }; - - key_event.set_chr(keycode); - key_event.down = down_or_up; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if get_key_state(enigo::Key::CapsLock) { - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if get_key_state(enigo::Key::NumLock) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } - self.send_key_event(key_event, KeyboardMode::Map); - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn translate_keyboard_mode(&self, down_or_up: bool, key: RdevKey, evt: Event) { - // translate mode(2): locally generated characters are send to the peer. - - // get char - let string = match KEYBOARD.lock() { - Ok(mut keyboard) => { - let string = keyboard.add(&evt.event_type).unwrap_or_default(); - if keyboard.is_dead() && string == "" && down_or_up == true { - return; - } - string - } - Err(_) => "".to_owned(), - }; - - // maybe two string - let chars = if string == "" { - None - } else { - let chars: Vec = string.chars().collect(); - Some(chars) - }; - - if let Some(chars) = chars { - for chr in chars { - let mut key_event = KeyEvent::new(); - key_event.set_chr(chr as _); - key_event.down = true; - key_event.press = false; - - self.send_key_event(key_event, KeyboardMode::Translate); - } - } else { - let success = if down_or_up == true { - TO_RELEASE.lock().unwrap().insert(key) - } else { - TO_RELEASE.lock().unwrap().remove(&key) - }; - - // AltGr && LeftControl(SpecialKey) without action - if key == RdevKey::AltGr || evt.scan_code == 541 { - return; - } - if success { - self.map_keyboard_mode(down_or_up, key, None); - } - } - } - - fn legacy_modifiers( - &self, - key_event: &mut KeyEvent, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - if alt - && !crate::is_control_key(&key_event, &ControlKey::Alt) - && !crate::is_control_key(&key_event, &ControlKey::RAlt) - { - key_event.modifiers.push(ControlKey::Alt.into()); - } - if shift - && !crate::is_control_key(&key_event, &ControlKey::Shift) - && !crate::is_control_key(&key_event, &ControlKey::RShift) - { - key_event.modifiers.push(ControlKey::Shift.into()); - } - if ctrl - && !crate::is_control_key(&key_event, &ControlKey::Control) - && !crate::is_control_key(&key_event, &ControlKey::RControl) - { - key_event.modifiers.push(ControlKey::Control.into()); - } - if command - && !crate::is_control_key(&key_event, &ControlKey::Meta) - && !crate::is_control_key(&key_event, &ControlKey::RWin) - { - key_event.modifiers.push(ControlKey::Meta.into()); - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if get_key_state(enigo::Key::CapsLock) { - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.peer_platform() != "Mac OS" { - if get_key_state(enigo::Key::NumLock) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } - } - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn legacy_keyboard_mode(&self, down_or_up: bool, key: RdevKey, evt: Event) { - // legacy mode(0): Generate characters locally, look for keycode on other side. - let peer = self.peer_platform(); - let is_win = peer == "Windows"; - - let alt = get_key_state(enigo::Key::Alt); - #[cfg(windows)] - let ctrl = { - let mut tmp = - get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); - unsafe { - if IS_ALT_GR { - if alt || key == RdevKey::AltGr { - if tmp { - tmp = false; - } - } else { - IS_ALT_GR = false; - } - } - } - tmp - }; - #[cfg(not(windows))] - let ctrl = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); - let shift = get_key_state(enigo::Key::Shift) || get_key_state(enigo::Key::RightShift); - #[cfg(windows)] - let command = crate::platform::windows::get_win_key_state(); - #[cfg(not(windows))] - let command = get_key_state(enigo::Key::Meta); - let control_key = match key { - RdevKey::Alt => Some(ControlKey::Alt), - RdevKey::AltGr => Some(ControlKey::RAlt), - RdevKey::Backspace => Some(ControlKey::Backspace), - RdevKey::ControlLeft => { - // when pressing AltGr, an extra VK_LCONTROL with a special - // scancode with bit 9 set is sent, let's ignore this. - #[cfg(windows)] - if evt.scan_code & 0x200 != 0 { - unsafe { - IS_ALT_GR = true; - } - return; - } - Some(ControlKey::Control) - } - RdevKey::ControlRight => Some(ControlKey::RControl), - RdevKey::DownArrow => Some(ControlKey::DownArrow), - RdevKey::Escape => Some(ControlKey::Escape), - RdevKey::F1 => Some(ControlKey::F1), - RdevKey::F10 => Some(ControlKey::F10), - RdevKey::F11 => Some(ControlKey::F11), - RdevKey::F12 => Some(ControlKey::F12), - RdevKey::F2 => Some(ControlKey::F2), - RdevKey::F3 => Some(ControlKey::F3), - RdevKey::F4 => Some(ControlKey::F4), - RdevKey::F5 => Some(ControlKey::F5), - RdevKey::F6 => Some(ControlKey::F6), - RdevKey::F7 => Some(ControlKey::F7), - RdevKey::F8 => Some(ControlKey::F8), - RdevKey::F9 => Some(ControlKey::F9), - RdevKey::LeftArrow => Some(ControlKey::LeftArrow), - RdevKey::MetaLeft => Some(ControlKey::Meta), - RdevKey::MetaRight => Some(ControlKey::RWin), - RdevKey::Return => Some(ControlKey::Return), - RdevKey::RightArrow => Some(ControlKey::RightArrow), - RdevKey::ShiftLeft => Some(ControlKey::Shift), - RdevKey::ShiftRight => Some(ControlKey::RShift), - RdevKey::Space => Some(ControlKey::Space), - RdevKey::Tab => Some(ControlKey::Tab), - RdevKey::UpArrow => Some(ControlKey::UpArrow), - RdevKey::Delete => { - if is_win && ctrl && alt { - self.ctrl_alt_del(); - return; - } - Some(ControlKey::Delete) - } - RdevKey::Apps => Some(ControlKey::Apps), - RdevKey::Cancel => Some(ControlKey::Cancel), - RdevKey::Clear => Some(ControlKey::Clear), - RdevKey::Kana => Some(ControlKey::Kana), - RdevKey::Hangul => Some(ControlKey::Hangul), - RdevKey::Junja => Some(ControlKey::Junja), - RdevKey::Final => Some(ControlKey::Final), - RdevKey::Hanja => Some(ControlKey::Hanja), - RdevKey::Hanji => Some(ControlKey::Hanja), - RdevKey::Convert => Some(ControlKey::Convert), - RdevKey::Print => Some(ControlKey::Print), - RdevKey::Select => Some(ControlKey::Select), - RdevKey::Execute => Some(ControlKey::Execute), - RdevKey::PrintScreen => Some(ControlKey::Snapshot), - RdevKey::Help => Some(ControlKey::Help), - RdevKey::Sleep => Some(ControlKey::Sleep), - RdevKey::Separator => Some(ControlKey::Separator), - RdevKey::KpReturn => Some(ControlKey::NumpadEnter), - RdevKey::Kp0 => Some(ControlKey::Numpad0), - RdevKey::Kp1 => Some(ControlKey::Numpad1), - RdevKey::Kp2 => Some(ControlKey::Numpad2), - RdevKey::Kp3 => Some(ControlKey::Numpad3), - RdevKey::Kp4 => Some(ControlKey::Numpad4), - RdevKey::Kp5 => Some(ControlKey::Numpad5), - RdevKey::Kp6 => Some(ControlKey::Numpad6), - RdevKey::Kp7 => Some(ControlKey::Numpad7), - RdevKey::Kp8 => Some(ControlKey::Numpad8), - RdevKey::Kp9 => Some(ControlKey::Numpad9), - RdevKey::KpDivide => Some(ControlKey::Divide), - RdevKey::KpMultiply => Some(ControlKey::Multiply), - RdevKey::KpDecimal => Some(ControlKey::Decimal), - RdevKey::KpMinus => Some(ControlKey::Subtract), - RdevKey::KpPlus => Some(ControlKey::Add), - RdevKey::CapsLock | RdevKey::NumLock | RdevKey::ScrollLock => { - return; - } - RdevKey::Home => Some(ControlKey::Home), - RdevKey::End => Some(ControlKey::End), - RdevKey::Insert => Some(ControlKey::Insert), - RdevKey::PageUp => Some(ControlKey::PageUp), - RdevKey::PageDown => Some(ControlKey::PageDown), - RdevKey::Pause => Some(ControlKey::Pause), - _ => None, - }; - let mut key_event = KeyEvent::new(); - if let Some(k) = control_key { - key_event.set_control_key(k); - } else { - let mut chr = match evt.name { - Some(ref s) => { - if s.len() <= 2 { - // exclude chinese characters - s.chars().next().unwrap_or('\0') - } else { - '\0' - } - } - _ => '\0', - }; - if chr == '·' { - // special for Chinese - chr = '`'; - } - if chr == '\0' { - chr = match key { - RdevKey::Num1 => '1', - RdevKey::Num2 => '2', - RdevKey::Num3 => '3', - RdevKey::Num4 => '4', - RdevKey::Num5 => '5', - RdevKey::Num6 => '6', - RdevKey::Num7 => '7', - RdevKey::Num8 => '8', - RdevKey::Num9 => '9', - RdevKey::Num0 => '0', - RdevKey::KeyA => 'a', - RdevKey::KeyB => 'b', - RdevKey::KeyC => 'c', - RdevKey::KeyD => 'd', - RdevKey::KeyE => 'e', - RdevKey::KeyF => 'f', - RdevKey::KeyG => 'g', - RdevKey::KeyH => 'h', - RdevKey::KeyI => 'i', - RdevKey::KeyJ => 'j', - RdevKey::KeyK => 'k', - RdevKey::KeyL => 'l', - RdevKey::KeyM => 'm', - RdevKey::KeyN => 'n', - RdevKey::KeyO => 'o', - RdevKey::KeyP => 'p', - RdevKey::KeyQ => 'q', - RdevKey::KeyR => 'r', - RdevKey::KeyS => 's', - RdevKey::KeyT => 't', - RdevKey::KeyU => 'u', - RdevKey::KeyV => 'v', - RdevKey::KeyW => 'w', - RdevKey::KeyX => 'x', - RdevKey::KeyY => 'y', - RdevKey::KeyZ => 'z', - RdevKey::Comma => ',', - RdevKey::Dot => '.', - RdevKey::SemiColon => ';', - RdevKey::Quote => '\'', - RdevKey::LeftBracket => '[', - RdevKey::RightBracket => ']', - RdevKey::Slash => '/', - RdevKey::BackSlash => '\\', - RdevKey::Minus => '-', - RdevKey::Equal => '=', - RdevKey::BackQuote => '`', - _ => '\0', - } - } - if chr != '\0' { - if chr == 'l' && is_win && command { - self.lock_screen(); - return; - } - key_event.set_chr(chr as _); - } else { - log::error!("Unknown key {:?}", evt); - return; - } - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let (alt, ctrl, shift, command) = get_all_hotkey_state(alt, ctrl, shift, command); - self.legacy_modifiers(&mut key_event, alt, ctrl, shift, command); - - if down_or_up == true { - key_event.down = true; - } - self.send_key_event(key_event, KeyboardMode::Legacy) - } - - fn key_down_or_up(&self, down_or_up: bool, key: RdevKey, evt: Event) { - // Call different functions according to keyboard mode. - let mode = match self.get_keyboard_mode().as_str() { - "map" => KeyboardMode::Map, - "legacy" => KeyboardMode::Legacy, - "translate" => KeyboardMode::Translate, - _ => KeyboardMode::Legacy, - }; - - #[cfg(not(windows))] - let key = self.convert_numpad_keys(key); - - let mut to_release = TO_RELEASE.lock().unwrap(); - match mode { - KeyboardMode::Map => { - if down_or_up == true { - to_release.insert(key); - } else { - to_release.remove(&key); - } - self.map_keyboard_mode(down_or_up, key, Some(evt)); - } - KeyboardMode::Legacy => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.legacy_keyboard_mode(down_or_up, key, evt) - } - KeyboardMode::Translate => { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.translate_keyboard_mode(down_or_up, key, evt); - } - _ => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.legacy_keyboard_mode(down_or_up, key, evt) - } - } - } - pub fn get_platform(&self, is_remote: bool) -> String { if is_remote { self.peer_platform() @@ -768,6 +319,13 @@ impl Session { return "".to_owned(); } + pub fn send_key_event(&self, evt: &KeyEvent) { + // mode: legacy(0), map(1), translate(2), auto(3) + let mut msg_out = Message::new(); + msg_out.set_key_event(evt.clone()); + self.send(Data::Message(msg_out)); + } + pub fn send_chat(&self, text: String) { let mut misc = Misc::new(); misc.set_chat_message(ChatMessage { @@ -790,77 +348,14 @@ impl Session { self.send(Data::Message(msg_out)); } - pub fn lock_screen(&self) { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::LockScreen); - // todo - key_event.down = true; - self.send_key_event(key_event, KeyboardMode::Legacy); - } - pub fn enter(&self) { IS_IN.store(true, Ordering::SeqCst); - #[cfg(target_os = "linux")] - self.grab_hotkeys(true); - - #[cfg(windows)] - crate::platform::windows::stop_system_key_propagate(true); + keyboard::client::change_grab_status(GrabState::Run); } pub fn leave(&self) { IS_IN.store(false, Ordering::SeqCst); - #[cfg(target_os = "linux")] - self.grab_hotkeys(false); - - for key in TO_RELEASE.lock().unwrap().iter() { - self.map_keyboard_mode(false, *key, None) - } - #[cfg(windows)] - crate::platform::windows::stop_system_key_propagate(false); - } - - #[cfg(target_os = "linux")] - pub fn grab_hotkeys(&self, _grab: bool) { - if _grab { - rdev::enable_grab().ok(); - } else { - rdev::disable_grab().ok(); - } - } - - pub fn handle_flutter_key_event( - &self, - name: &str, - keycode: i32, - scancode: i32, - down_or_up: bool, - ) { - if scancode < 0 || keycode < 0 { - return; - } - let keycode: u32 = keycode as u32; - let scancode: u32 = scancode as u32; - - #[cfg(not(target_os = "windows"))] - let key = rdev::key_from_scancode(scancode) as RdevKey; - // Windows requires special handling - #[cfg(target_os = "windows")] - let key = rdev::get_win_key(keycode, scancode); - - let event_type = if down_or_up { - KeyPress(key) - } else { - KeyRelease(key) - }; - let evt = Event { - time: std::time::SystemTime::now(), - name: Option::Some(name.to_owned()), - code: keycode as _, - scan_code: scancode as _, - event_type: event_type, - }; - - self.key_down_or_up(down_or_up, key, evt) + keyboard::client::change_grab_status(GrabState::Wait); } // flutter only TODO new input @@ -874,9 +369,6 @@ impl Session { shift: bool, command: bool, ) { - if HOTKEY_HOOKED.load(Ordering::SeqCst) { - return; - } let chars: Vec = name.chars().collect(); if chars.len() == 1 { let key = Key::_Raw(chars[0] as _); @@ -897,6 +389,40 @@ impl Session { self.send(Data::Message(msg_out)); } + pub fn handle_flutter_key_event( + &self, + name: &str, + keycode: i32, + scancode: i32, + down_or_up: bool, + ) { + if scancode < 0 || keycode < 0 { + return; + } + let keycode: u32 = keycode as u32; + let scancode: u32 = scancode as u32; + + #[cfg(not(target_os = "windows"))] + let key = rdev::key_from_code(keycode) as rdev::Key; + // Windows requires special handling + #[cfg(target_os = "windows")] + let key = rdev::get_win_key(keycode, scancode); + + let event_type = if down_or_up { + KeyPress(key) + } else { + KeyRelease(key) + }; + let event = Event { + time: std::time::SystemTime::now(), + name: Option::Some(name.to_owned()), + code: keycode as _, + scan_code: scancode as _, + event_type: event_type, + }; + keyboard::client::process_event(&event); + } + // flutter only TODO new input fn _input_key( &self, @@ -921,25 +447,6 @@ impl Session { key_event.set_chr(chr); } Key::ControlKey(key) => { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let key = if !get_key_state(enigo::Key::NumLock) { - match key { - ControlKey::Numpad0 => ControlKey::Insert, - ControlKey::Decimal => ControlKey::Delete, - ControlKey::Numpad1 => ControlKey::End, - ControlKey::Numpad2 => ControlKey::DownArrow, - ControlKey::Numpad3 => ControlKey::PageDown, - ControlKey::Numpad4 => ControlKey::LeftArrow, - ControlKey::Numpad5 => ControlKey::Clear, - ControlKey::Numpad6 => ControlKey::RightArrow, - ControlKey::Numpad7 => ControlKey::Home, - ControlKey::Numpad8 => ControlKey::UpArrow, - ControlKey::Numpad9 => ControlKey::PageUp, - _ => key, - } - } else { - key - }; key_event.set_control_key(key.clone()); } Key::_Raw(raw) => { @@ -947,17 +454,15 @@ impl Session { } } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let (alt, ctrl, shift, command) = get_all_hotkey_state(alt, ctrl, shift, command); - - self.legacy_modifiers(&mut key_event, alt, ctrl, shift, command); if v == 1 { key_event.down = true; } else if v == 3 { key_event.press = true; } + keyboard::client::legacy_modifiers(&mut key_event, alt, ctrl, shift, command); + key_event.mode = KeyboardMode::Legacy.into(); - self.send_key_event(key_event, KeyboardMode::Legacy); + self.send_key_event(&key_event); } pub fn send_mouse( @@ -979,8 +484,9 @@ impl Session { } } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let (alt, ctrl, shift, command) = get_all_hotkey_state(alt, ctrl, shift, command); + // #[cfg(not(any(target_os = "android", target_os = "ios")))] + let (alt, ctrl, shift, command) = + keyboard::client::get_modifiers_state(alt, ctrl, shift, command); send_mouse(mask, x, y, alt, ctrl, shift, command, self); // on macos, ctrl + left button down = right button down, up won't emit, so we need to @@ -1098,9 +604,10 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_cursor_data(&self, cd: CursorData); fn set_cursor_id(&self, id: String); fn set_cursor_position(&self, cp: CursorPosition); - fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embeded: bool); + fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool); fn switch_display(&self, display: &SwitchDisplay); fn set_peer_info(&self, peer_info: &PeerInfo); // flutter + fn on_connected(&self, conn_type: ConnType); fn update_privacy_mode(&self); fn set_permission(&self, name: &str, value: bool); fn close_success(&self); @@ -1150,39 +657,26 @@ impl FileManager for Session {} #[async_trait] impl Interface for Session { + fn get_login_config_handler(&self) -> Arc> { + return self.lc.clone(); + } + fn send(&self, data: Data) { if let Some(sender) = self.sender.read().unwrap().as_ref() { sender.send(data).ok(); } } - fn is_file_transfer(&self) -> bool { - self.lc - .read() - .unwrap() - .conn_type - .eq(&ConnType::FILE_TRANSFER) - } - - fn is_port_forward(&self) -> bool { - self.lc - .read() - .unwrap() - .conn_type - .eq(&ConnType::PORT_FORWARD) - } - - fn is_rdp(&self) -> bool { - self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) - } - fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { - let retry = check_if_retry(msgtype, title, text); + let direct = self.lc.read().unwrap().direct.unwrap_or_default(); + let received = self.lc.read().unwrap().received; + let retry_for_relay = direct && !received; + let retry = check_if_retry(msgtype, title, text, retry_for_relay); self.ui_handler.msgbox(msgtype, title, text, link, retry); } fn handle_login_error(&mut self, err: &str) -> bool { - self.lc.write().unwrap().handle_login_error(err, self) + handle_login_error(self.lc.clone(), err, self) } fn handle_peer_info(&mut self, mut pi: PeerInfo) { @@ -1216,7 +710,7 @@ impl Interface for Session { current.y, current.width, current.height, - current.cursor_embeded, + current.cursor_embedded, ); } self.update_privacy_mode(); @@ -1233,6 +727,7 @@ impl Interface for Session { "", ); } + self.on_connected(self.lc.read().unwrap().conn_type); #[cfg(windows)] { let mut path = std::env::temp_dir(); @@ -1243,23 +738,6 @@ impl Interface for Session { crate::platform::windows::add_recent_document(&path); } } - // only run in sciter - #[cfg(not(feature = "flutter"))] - { - // rdev::grab and rdev::listen use the same api in macOS & Windows - /* todo! Unused */ - #[cfg(not(any( - target_os = "android", - target_os = "ios", - target_os = "macos", - target_os = "windows", - target_os = "linux", - )))] - self.start_keyboard_hook(); - /* todo! (sciter) Only one device can be connected at the same time in linux */ - #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.start_grab_hotkey(); - } } async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { @@ -1280,133 +758,14 @@ impl Interface for Session { handle_test_delay(t, peer).await; } } - - fn set_force_relay(&mut self, direct: bool, received: bool) { - let mut lc = self.lc.write().unwrap(); - lc.force_relay = false; - if direct && !received { - let errno = errno::errno().0; - log::info!("errno is {}", errno); - // TODO: check mac and ios - if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { - lc.force_relay = true; - lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); - } - } - } - - fn is_force_relay(&self) -> bool { - self.lc.read().unwrap().force_relay - } } -#[cfg(not(any(target_os = "android", target_os = "ios")))] impl Session { - fn handle_hotkey_event(&self, event: Event) { - // if is long press, don't do anything. - if is_long_press(&event) { - return; - } - - let (key, down) = match event.event_type { - EventType::KeyPress(key) => (key, true), - EventType::KeyRelease(key) => (key, false), - _ => return, - }; - - self.key_down_or_up(down, key, event); + pub fn lock_screen(&self) { + crate::keyboard::client::lock_screen(); } - - #[allow(dead_code)] - fn start_grab_hotkey(&self) { - if self.is_port_forward() || self.is_file_transfer() { - return; - } - #[cfg(target_os = "linux")] - if !*IS_X11.lock().unwrap() { - return; - } - if HOTKEY_HOOKED.swap(true, Ordering::SeqCst) { - return; - } - - log::info!("starting grab hotkeys"); - let me = self.clone(); - - #[cfg(target_os = "linux")] - { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(_key) | EventType::KeyRelease(_key) => { - me.handle_hotkey_event(event); - None - } - _ => Some(event), - }; - rdev::start_grab_listen(func) - } - #[cfg(any(target_os = "windows", target_os = "macos"))] - std::thread::spawn(move || { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(..) | EventType::KeyRelease(..) => { - // grab all keys - if !IS_IN.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - { - return Some(event); - } else { - me.handle_hotkey_event(event); - return None; - } - } - _ => Some(event), - }; - if let Err(error) = rdev::grab(func) { - log::error!("Error: {:?}", error) - } - }); - } - - #[allow(dead_code)] - fn start_keyboard_hook(&self) { - // only run in sciter - if self.is_port_forward() || self.is_file_transfer() { - return; - } - if KEYBOARD_HOOKED.swap(true, Ordering::SeqCst) { - return; - } - log::info!("keyboard hooked"); - - let me = self.clone(); - #[cfg(windows)] - crate::platform::windows::enable_lowlevel_keyboard(std::ptr::null_mut() as _); - std::thread::spawn(move || { - // This will block. - std::env::set_var("KEYBOARD_ONLY", "y"); - - let func = move |evt: Event| { - /* todo! IS_IN can't determine if the user is focused on remote page */ - if !IS_IN.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - { - return; - } - if is_long_press(&evt) { - return; - } - let (key, down) = match evt.event_type { - EventType::KeyPress(key) => (key, true), - EventType::KeyRelease(key) => (key, false), - _ => return, - }; - me.key_down_or_up(down, key, evt); - }; - /* todo!: Shift + a -> AA in sciter - * rdev::listen and rdev::grab both send a - */ - if let Err(error) = rdev::listen(func) { - log::error!("rdev: {:?}", error); - } - }); + pub fn ctrl_alt_del(&self) { + crate::keyboard::client::ctrl_alt_del(); } } @@ -1560,107 +919,3 @@ async fn send_note(url: String, id: String, conn_id: i32, note: String) { let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); allow_err!(crate::post_request(url, body.to_string(), "").await); } - -fn get_hotkey_state(key: RdevKey) -> bool { - if let Some(&state) = MUTEX_SPECIAL_KEYS.lock().unwrap().get(&key) { - return state; - } else { - return false; - } -} - -fn get_all_hotkey_state( - alt: bool, - ctrl: bool, - shift: bool, - command: bool, -) -> (bool, bool, bool, bool) { - let ctrl = - get_hotkey_state(RdevKey::ControlLeft) || get_hotkey_state(RdevKey::ControlRight) || ctrl; - let shift = - get_hotkey_state(RdevKey::ShiftLeft) || get_hotkey_state(RdevKey::ShiftRight) || shift; - let command = - get_hotkey_state(RdevKey::MetaLeft) || get_hotkey_state(RdevKey::MetaRight) || command; - let alt = get_hotkey_state(RdevKey::Alt) || get_hotkey_state(RdevKey::AltGr) || alt; - - (alt, ctrl, shift, command) -} - -#[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn send_key_event_to_session(event: rdev::Event) { - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - handler.handle_hotkey_event(event); - } -} - -#[cfg(feature = "flutter")] -pub fn global_grab_keyboard() { - if HOTKEY_HOOKED.swap(true, Ordering::SeqCst) { - return; - } - log::info!("starting global grab keyboard"); - - #[cfg(target_os = "linux")] - { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(_key) | EventType::KeyRelease(_key) => { - send_key_event_to_session(event); - None - } - _ => Some(event), - }; - rdev::start_grab_listen(func) - } - - #[cfg(any(target_os = "windows", target_os = "macos"))] - std::thread::spawn(move || { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(..) | EventType::KeyRelease(..) => { - // grab all keys - if !IS_IN.load(Ordering::SeqCst) { - return Some(event); - } else { - send_key_event_to_session(event); - return None; - } - } - _ => Some(event), - }; - if let Err(error) = rdev::grab(func) { - log::error!("Error: {:?}", error) - } - }); -} - -pub fn global_get_keyboard_mode() -> String { - return std::env::var("KEYBOARD_MODE") - .unwrap_or(String::from("map")) - .to_lowercase(); -} - -pub fn global_save_keyboard_mode(value: String) { - std::env::set_var("KEYBOARD_MODE", value); -} - -fn is_long_press(event: &Event) -> bool { - let mut keys = MUTEX_SPECIAL_KEYS.lock().unwrap(); - match event.event_type { - EventType::KeyPress(k) => { - if let Some(&state) = keys.get(&k) { - if state == true { - return true; - } else { - keys.insert(k, true); - } - } - } - EventType::KeyRelease(k) => { - if keys.contains_key(&k) { - keys.insert(k, false); - } - } - _ => {} - }; - return false; -}