diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 8b58db83f..7825286bd 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -38,7 +38,7 @@ jobs: shell: bash run: | case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libayatana-appindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 3062001de..fae447d6b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -11,6 +11,7 @@ env: FLUTTER_VERSION: "3.0.5" TAG_NAME: "nightly" VCPKG_COMMIT_ID: '6ca56aeb457f033d344a7106cb3f9f1abf8f4e98' + VERSION: "1.2.0" jobs: build-for-windows: @@ -92,7 +93,7 @@ jobs: rustdesk-*.exe build-for-linux: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + name: ${{ matrix.job.target }} (${{ matrix.job.os }},${{ matrix.job.extra-build-args }}) runs-on: ${{ matrix.job.os }} strategy: fail-fast: false @@ -104,7 +105,8 @@ jobs: # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } # - { target: x86_64-apple-darwin , os: macos-10.15 } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04} + - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04, extra-build-args: ""} + - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04, extra-build-args: "--flatpak"} # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - name: Checkout source code @@ -113,7 +115,7 @@ jobs: - name: Install prerequisites run: | case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libayatana-appindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev;; # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac @@ -159,7 +161,7 @@ jobs: - name: Install cargo bundle tools run: | - cargo install cargo-bundle --force + cargo install cargo-bundle - name: Show version information (Rust, cargo, GCC) shell: bash @@ -172,7 +174,7 @@ jobs: rustc -V - name: Build rustdesk - run: ./build.py --flutter --hwcodec + run: ./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }} - name: Rename rustdesk shell: bash @@ -187,9 +189,17 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - rustdesk*.deb + rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Upload Artifcat + uses: actions/upload-artifact@master + if: ${{ contains(matrix.job.extra-build-args, 'flatpak') }} + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - name: Build archlinux package + if: ${{ matrix.job.extra-build-args == '' }} uses: vufa/arch-makepkg-action@master with: packages: > @@ -220,14 +230,85 @@ jobs: python ttf-arphic-uming libappindicator-gtk3 - libayatana-appindicator scripts: | cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f - name: Publish archlinux package + if: ${{ matrix.job.extra-build-args == '' }} uses: softprops/action-gh-release@v1 with: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | res/rustdesk*.zst + + # - name: build RPM package + # id: rpm + # uses: Kingtous/rustdesk-rpmbuild@master + # with: + # spec_file: "res/rpm-flutter.spec" + + # - name: Publish fedora28/centos8 package + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # ${{ steps.rpm.outputs.rpm_dir_path }}/* + + build-flatpak: + name: Build Flatpak + needs: [build-for-linux] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + # - { target: x86_64-apple-darwin , os: macos-10.15 } + - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04, arch: x86_64} + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev + + - name: Download Binary + uses: actions/download-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: . + + - name: Rename Binary + run: | + mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb + + - name: Install Flatpak deps + run: | + flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 + flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 + + - name: Make Flatpak package + run: | + pushd flatpak + git clone https://github.com/flathub/shared-modules.git --depth=1 + flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk + + - name: Publish flatpak package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak + diff --git a/.gitignore b/.gitignore index d1e9666bd..1ecea7af8 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,6 @@ flatpak/.flatpak-builder/build/** flatpak/.flatpak-builder/shared-modules/** flatpak/.flatpak-builder/shared-modules/*.tar.xz flatpak/.flatpak-builder/debian-binary +flatpak/build/** # bridge file lib/generated_bridge.dart diff --git a/Cargo.lock b/Cargo.lock index 26ed9455f..9243933e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2463,7 +2463,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.1.0" -source = "git+https://github.com/21pages/hwcodec#1f03d203eca24dc976c21a47228f3bc31484c2bc" +source = "git+https://github.com/21pages/hwcodec#bf73e8e650abca3e004e96a245086b3647b9d84a" dependencies = [ "bindgen", "cc", @@ -4419,6 +4419,7 @@ dependencies = [ "system_shutdown", "tray-item", "trayicon", + "url", "uuid", "virtual_display", "whoami", @@ -5492,6 +5493,7 @@ dependencies = [ "idna", "matches", "percent-encoding", + "serde 1.0.144", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c950c8723..5dc54d58b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,9 +64,9 @@ 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/asur4s/rdev" } +url = { version = "2.1", features = ["serde"] } -[target.'cfg(not(target_os = "linux"))'.dependencies] -reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features=false } +reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.13.5" @@ -147,7 +147,7 @@ hound = "3.5" name = "RustDesk" identifier = "com.carriez.rustdesk" icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"] -deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pipewire", "curl", "libayatana-appindicator3-1", "libvdpau1", "libva2"] +deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "curl", "libappindicator3-1", "libvdpau1", "libva2"] osx_minimum_system_version = "10.14" resources = ["res/mac-tray-light.png","res/mac-tray-dark.png"] diff --git a/README.md b/README.md index 59eef0ba0..dfaa389a6 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Please download sciter dynamic library yourself. ```sh sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ - libclang-dev ninja-build libayatana-appindicator3-1 libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libayatana-appindicator3-dev + libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ``` ### openSUSE Tumbleweed diff --git a/build.py b/build.py index 3e7ce853e..c907334ce 100755 --- a/build.py +++ b/build.py @@ -81,6 +81,11 @@ def make_parser(): action='store_true', help='Build windows portable' ) + parser.add_argument( + '--flatpak', + action='store_true', + help='Build rustdesk libs with the flatpak feature enabled' + ) return parser @@ -188,6 +193,8 @@ def get_features(args): features.append('hwcodec') if args.flutter: features.append('flutter') + if args.flatpak: + features.append('flatpak') print("features:", features) return features @@ -201,7 +208,7 @@ Version: %s Architecture: amd64 Maintainer: open-trade Homepage: https://rustdesk.com -Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, pipewire, curl +Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, pipewire, curl, libappindicator3-1, libva-drm2, libva-x11-2, libvdpau1 Description: A remote control software. """ % version diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json new file mode 100644 index 000000000..d7f6e316e --- /dev/null +++ b/flatpak/rustdesk.json @@ -0,0 +1,38 @@ +{ + "app-id": "org.rustdesk.rustdesk", + "runtime": "org.freedesktop.Platform", + "runtime-version": "21.08", + "sdk": "org.freedesktop.Sdk", + "command": "rustdesk", + "modules": [ + "shared-modules/libappindicator/libappindicator-gtk3-12.10.json", + "xdotool.json", + { + "name": "rustdesk", + "buildsystem": "simple", + "build-commands": [ + "bsdtar -zxvf rustdesk-1.2.0.deb", + "tar -xvf ./data.tar.xz", + "cp -r ./usr /app/", + "mkdir -p /app/bin && ln -s /app/usr/lib/rustdesk/rustdesk /app/bin/rustdesk" + ], + "sources": [ + { + "type": "file", + "path": "../rustdesk-1.2.0.deb" + } + ] + } + ], + "finish-args": [ + "--share=ipc", + "--socket=x11", + "--socket=fallback-x11", + "--socket=wayland", + "--share=network", + "--filesystem=home", + "--device=dri", + "--socket=pulseaudio", + "--talk-name=org.freedesktop.Flatpak" + ] +} \ No newline at end of file diff --git a/flatpak/rustdesk.yml b/flatpak/rustdesk.yml deleted file mode 100644 index 3d7936635..000000000 --- a/flatpak/rustdesk.yml +++ /dev/null @@ -1,31 +0,0 @@ -app-id: org.rustdesk.rustdesk -runtime: org.freedesktop.Platform -runtime-version: '21.08' -sdk: org.freedesktop.Sdk -command: rustdesk -modules: - # install appindicator - - shared-modules/libappindicator/libappindicator-gtk3-12.10.json - - name: rustdesk - buildsystem: simple - build-commands: - - bsdtar -zxvf rustdesk-1.2.0.deb - - tar -xvf ./data.tar.xz - - cp -r ./usr /app/ - - rm /app/usr/bin/rustdesk - - mkdir -p /app/bin && ln -s /app/usr/lib/rustdesk/flutter_hbb /app/bin/rustdesk - sources: - # Note: replace to deb files with url - - type: file - path: ../rustdesk-1.2.0.deb - -finish-args: - # X11 + XShm access - - --share=ipc - - --socket=x11 - # Wayland access - - --socket=wayland - # Needs to talk to the network: - - --share=network - # Needs to save files locally - - --filesystem=xdg-documents \ No newline at end of file diff --git a/flatpak/xdotool.json b/flatpak/xdotool.json new file mode 100644 index 000000000..d7f41bf0e --- /dev/null +++ b/flatpak/xdotool.json @@ -0,0 +1,15 @@ +{ + "name": "xdotool", + "buildsystem": "simple", + "build-commands": [ + "make -j4 && PREFIX=./build make install", + "cp -r ./build/* /app/" + ], + "sources": [ + { + "type": "archive", + "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz", + "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada" + } + ] +} diff --git a/flutter/assets/Github.svg b/flutter/assets/Github.svg new file mode 100644 index 000000000..a5bd1de81 --- /dev/null +++ b/flutter/assets/Github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/Google.svg b/flutter/assets/Google.svg new file mode 100644 index 000000000..b7bb2f42f --- /dev/null +++ b/flutter/assets/Google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/Okta.svg b/flutter/assets/Okta.svg new file mode 100644 index 000000000..0fa45b93d --- /dev/null +++ b/flutter/assets/Okta.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ff69302f2..ec83e1123 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; import 'package:back_button_interceptor/back_button_interceptor.dart'; @@ -1036,6 +1037,7 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async { final isMaximized = await wc.isMaximized(); final pos = LastWindowPosition( sz.width, sz.height, position.dx, position.dy, isMaximized); + debugPrint("saving frame: ${windowId}: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}"); await Get.find() .setString(kWindowPrefix + type.name, pos.toString()); break; @@ -1081,7 +1083,7 @@ Future _adjustRestoreMainWindowSize(double? width, double? height) async { restoreWidth = maxWidth; } if (restoreHeight > maxHeight) { - restoreWidth = maxHeight; + restoreHeight = maxHeight; } return Size(restoreWidth, restoreHeight); } @@ -1092,11 +1094,11 @@ Future _adjustRestoreMainWindowOffset( if (left == null || top == null) { await windowManager.center(); } else { - double windowLeft = left; - double windowTop = top; + double windowLeft = max(0.0, left); + double windowTop = max(0.0, top); - double frameLeft = 0; - double frameTop = 0; + double frameLeft = double.infinity; + double frameTop = double.infinity; double frameRight = ((isDesktop || isWebDesktop) ? kDesktopMaxDisplayWidth : kMobileMaxDisplayWidth) @@ -1107,12 +1109,11 @@ Future _adjustRestoreMainWindowOffset( .toDouble(); if (isDesktop || isWebDesktop) { - final screen = (await window_size.getWindowInfo()).screen; - if (screen != null) { - frameLeft = screen.visibleFrame.left; - frameTop = screen.visibleFrame.top; - frameRight = screen.visibleFrame.right; - frameBottom = screen.visibleFrame.bottom; + for(final screen in await window_size.getScreenList()) { + frameLeft = min(screen.visibleFrame.left, frameLeft); + frameTop = min(screen.visibleFrame.top, frameTop); + frameRight = max(screen.visibleFrame.right, frameRight); + frameBottom = max(screen.visibleFrame.bottom, frameBottom); } } @@ -1174,6 +1175,7 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { await _adjustRestoreMainWindowSize(lpos.width, lpos.height); final offset = await _adjustRestoreMainWindowOffset( lpos.offsetWidth, lpos.offsetHeight); + debugPrint("restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}"); if (offset == null) { await wc.center(); } else { diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 570ff6e95..c96dc115a 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.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'; diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index ea1142033..8287378e9 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -21,7 +21,7 @@ const String kTabLabelSettingPage = "Settings"; const String kWindowPrefix = "wm_"; -// the executable name of the portable version +// the executable name of the portable version const String kEnvPortableExecutable = "RUSTDESK_APPNAME"; const Color kColorWarn = Color.fromARGB(255, 245, 133, 59); @@ -60,6 +60,12 @@ const kInvalidValueStr = "InvalidValueStr"; const kMobilePageConstraints = BoxConstraints(maxWidth: 600); +/// [kMouseControlDistance] indicates the distance that self-side move to get control of mouse. +const kMouseControlDistance = 12; + +/// [kMouseControlTimeoutMSec] indicates the timeout (in milliseconds) that self-side can get control of mouse. +const kMouseControlTimeoutMSec = 1000; + /// 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 506a03b7a..fc5b8e574 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -288,7 +288,7 @@ class _ConnectionPageState extends State offstage: !svcStopped.value, child: GestureDetector( onTap: () async { - bool checked = + bool checked = !bind.mainIsInstalled() || await bind.mainCheckSuperUserPermission(); if (checked) { bind.mainSetOption(key: "stop-service", value: ""); diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index ad0aa4316..e7d6f50e8 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -470,141 +470,6 @@ class _DesktopHomePageState extends State } } -/// common login dialog for desktop -/// call this directly -Future loginDialog() async { - String userName = ""; - var userNameMsg = ""; - String pass = ""; - var passMsg = ""; - var userController = TextEditingController(text: userName); - var pwdController = TextEditingController(text: pass); - - var isInProgress = false; - var completer = Completer(); - gFFI.dialogManager.show((setState, close) { - submit() async { - setState(() { - userNameMsg = ""; - passMsg = ""; - isInProgress = true; - }); - cancel() { - setState(() { - isInProgress = false; - }); - } - - userName = userController.text; - pass = pwdController.text; - 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) { - debugPrint(err.toString()); - cancel(); - return; - } - close(); - } - - cancel() { - completer.complete(false); - 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, - ), - 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, - ), - 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()) - ], - ), - ), - actions: [ - TextButton(onPressed: cancel, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: cancel, - ); - }); - return completer.future; -} - void setPasswordDialog() async { final pw = await bind.mainGetPermanentPassword(); final p0 = TextEditingController(text: pw); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index b063c3c15..12bb935e9 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.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'; diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 44440d4b1..d0f6c800e 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -96,8 +96,7 @@ class _FileManagerTabPageState extends State { void onRemoveId(String id) { if (tabController.state.value.tabs.isEmpty) { - WindowController.fromWindowId(windowId()).hide(); - rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId()}); + WindowController.fromWindowId(windowId()).close(); } } diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 94f652a76..403afe343 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -107,8 +107,7 @@ class _PortForwardTabPageState extends State { void onRemoveId(String id) { if (tabController.state.value.tabs.isEmpty) { - WindowController.fromWindowId(windowId()).hide(); - rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId()}); + WindowController.fromWindowId(windowId()).close(); } } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 94c54c7b5..3068f2db7 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -97,9 +97,6 @@ class _ConnectionTabPageState extends State { } _update_remote_count(); }); - Future.delayed(Duration.zero, () { - restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId()); - }); } @override @@ -321,10 +318,9 @@ class _ConnectionTabPageState extends State { ); } - void onRemoveId(String id) { + void onRemoveId(String id) async { if (tabController.state.value.tabs.isEmpty) { - WindowController.fromWindowId(windowId()).hide(); - rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId()}); + await WindowController.fromWindowId(windowId()).close(); } ConnectionTypeState.delete(id); _update_remote_count(); diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 884ac6441..e344575c7 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -134,6 +134,7 @@ class ConnectionManagerState extends State { showMaximize: false, showMinimize: true, showClose: true, + onWindowCloseButton: handleWindowCloseButton, controller: serverModel.tabController, maxLabelWidth: 100, tail: buildScrollJumper(), @@ -206,6 +207,27 @@ class ConnectionManagerState extends State { ], )); } + + Future handleWindowCloseButton() async { + var tabController = gFFI.serverModel.tabController; + final connLength = tabController.length; + if (connLength <= 1) { + windowManager.close(); + return true; + } else { + final opt = "enable-confirm-closing-tabs"; + final bool res; + if (!option2bool(opt, await bind.mainGetOption(key: opt))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + windowManager.close(); + } + return res; + } + } } Widget buildConnectionCard(Client client) { diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart new file mode 100644 index 000000000..3e58a6de2 --- /dev/null +++ b/flutter/lib/desktop/widgets/login.dart @@ -0,0 +1,521 @@ +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) { + debugPrint(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/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 3fcc087ee..b9e126c61 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -506,7 +506,7 @@ class WindowActionPanelState extends State rustDeskWinManager.unregisterActiveWindow(0); } else { widget.onClose?.call(); - WindowController.fromWindowId(windowId!).hide(); + await WindowController.fromWindowId(windowId!).hide(); rustDeskWinManager .call(WindowType.Main, kWindowEventHide, {"id": windowId!}); } @@ -555,12 +555,9 @@ class WindowActionPanelState extends State // note: the main window can be restored by tray icon Future.delayed(Duration.zero, () async { if (widget.isMainWindow) { - await windowManager.hide(); - rustDeskWinManager.unregisterActiveWindow(0); + await windowManager.close(); } else { - await WindowController.fromWindowId(windowId!).hide(); - rustDeskWinManager.call( - WindowType.Main, kWindowEventHide, {"id": windowId!}); + await WindowController.fromWindowId(windowId!).close(); } }); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 86bd04de1..ae15b5cf6 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -168,6 +168,20 @@ void runMultiWindow( widget, MyTheme.currentThemeMode(), ); + switch (appType) { + case kAppTypeDesktopRemote: + await restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId!); + break; + case kAppTypeDesktopFileTransfer: + await restoreWindowPosition(WindowType.FileTransfer, windowId: windowId!); + break; + case kAppTypeDesktopPortForward: + await restoreWindowPosition(WindowType.PortForward, windowId: windowId!); + break; + default: + // no such appType + exit(0); + } } void runConnectionManagerScreen() async { diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 7a82bcdd8..269439b1d 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -291,12 +291,12 @@ class _SettingsState extends State with WidgetsBindingObserver { return SettingsList( sections: [ SettingsSection( - title: Text(translate("Account")), + title: Text(translate('Account')), tiles: [ SettingsTile.navigation( title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty - ? translate("Login") - : '${translate("Logout")} (${gFFI.userModel.userName.value})')), + ? translate('Login') + : '${translate('Logout')} (${gFFI.userModel.userName.value})')), leading: Icon(Icons.person), onPressed: (context) { if (gFFI.userModel.userName.value.isEmpty) { diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index bc6582617..18a0be279 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:window_manager/window_manager.dart'; +import '../consts.dart'; import '../common.dart'; import '../common/widgets/overlay.dart'; import 'model.dart'; @@ -183,8 +184,11 @@ class ChatModel with ChangeNotifier { if (_isShowCMChatPage) { _isShowCMChatPage = !_isShowCMChatPage; notifyListeners(); - await windowManager.setSizeAlignment(Size(300, 400), Alignment.topRight); + await windowManager.show(); + await windowManager.setSizeAlignment( + kConnectionManagerWindowSize, Alignment.topRight); } else { + await windowManager.show(); await windowManager.setSizeAlignment(Size(600, 400), Alignment.topRight); _isShowCMChatPage = !_isShowCMChatPage; notifyListeners(); diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 280c72e79..30a01cda7 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -42,6 +42,7 @@ class InputModel { // mouse final isPhysicalMouse = false.obs; int _lastMouseDownButtons = 0; + Offset last_mouse_pos = Offset.zero; get id => parent.target?.id ?? ""; @@ -303,6 +304,28 @@ class InputModel { } void handleMouse(Map evt) { + double x = evt['x']; + double y = max(0.0, evt['y']); + final cursorModel = parent.target!.cursorModel; + + if (cursorModel.is_peer_control_protected) { + last_mouse_pos = ui.Offset(x, y); + return; + } + + if (!cursorModel.got_mouse_control) { + bool self_get_control = + (x - last_mouse_pos.dx).abs() > kMouseControlDistance || + (y - last_mouse_pos.dy).abs() > kMouseControlDistance; + if (self_get_control) { + cursorModel.got_mouse_control = true; + } else { + last_mouse_pos = ui.Offset(x, y); + return; + } + } + last_mouse_pos = ui.Offset(x, y); + var type = ''; var isMove = false; switch (evt['type']) { @@ -319,8 +342,6 @@ class InputModel { return; } evt['type'] = type; - double x = evt['x']; - double y = max(0.0, evt['y']); if (isDesktop) { y = y - stateGlobal.tabBarHeight; } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d8fdca2bc..be97b41ff 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -740,6 +740,9 @@ class CursorModel with ChangeNotifier { double _hoty = 0; double _displayOriginX = 0; double _displayOriginY = 0; + bool got_mouse_control = true; + DateTime _last_peer_mouse = DateTime.now() + .subtract(Duration(milliseconds: 2 * kMouseControlTimeoutMSec)); String id = ''; WeakReference parent; @@ -748,15 +751,17 @@ class CursorModel with ChangeNotifier { CursorData? get defaultCache => _getDefaultCache(); double get x => _x - _displayOriginX; - double get y => _y - _displayOriginY; Offset get offset => Offset(_x, _y); double get hotx => _hotx; - double get hoty => _hoty; + bool get is_peer_control_protected => + DateTime.now().difference(_last_peer_mouse).inMilliseconds < + kMouseControlTimeoutMSec; + CursorModel(this.parent); Set get cachedKeys => _cacheKeys; @@ -918,7 +923,7 @@ class CursorModel with ChangeNotifier { if (parent.target?.id != pid) return; _image = image; _images[id] = Tuple3(image, _hotx, _hoty); - await _updateCacheLinux(image, id, width, height); + await _updateCache(image, id, width, height); try { // my throw exception, because the listener maybe already dispose notifyListeners(); @@ -927,7 +932,7 @@ class CursorModel with ChangeNotifier { } } - _updateCacheLinux(ui.Image image, int id, int w, int h) async { + _updateCache(ui.Image image, int id, int w, int h) async { Uint8List? data; img2.Image? image2; if (Platform.isWindows) { @@ -981,6 +986,8 @@ class CursorModel with ChangeNotifier { /// Update the cursor position. updateCursorPosition(Map evt, String id) async { + got_mouse_control = false; + _last_peer_mouse = DateTime.now(); _x = double.parse(evt['x']); _y = double.parse(evt['y']); try { diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 721aac5b5..d2e83990b 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; @@ -9,7 +10,7 @@ import 'model.dart'; import 'platform_model.dart'; class UserModel { - var userName = "".obs; + var userName = ''.obs; WeakReference parent; UserModel(this.parent) { @@ -18,7 +19,7 @@ class UserModel { void refreshCurrentUser() async { await getUserName(); - final token = await bind.mainGetLocalOption(key: "access_token"); + final token = await bind.mainGetLocalOption(key: 'access_token'); if (token == '') return; final url = await bind.mainGetApiServer(); final body = { @@ -28,8 +29,8 @@ class UserModel { try { final response = await http.post(Uri.parse('$url/api/currentUser'), headers: { - "Content-Type": "application/json", - "Authorization": "Bearer $token" + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $token' }, body: json.encode(body)); final status = response.statusCode; @@ -44,9 +45,9 @@ class UserModel { } void resetToken() async { - await bind.mainSetLocalOption(key: "access_token", value: ""); - await bind.mainSetLocalOption(key: "user_info", value: ""); - userName.value = ""; + await bind.mainSetLocalOption(key: 'access_token', value: ''); + await bind.mainSetLocalOption(key: 'user_info', value: ''); + userName.value = ''; } Future _parseResp(String body) async { @@ -57,13 +58,13 @@ class UserModel { } final token = data['access_token']; if (token != null) { - await bind.mainSetLocalOption(key: "access_token", value: token); + 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"]; + await bind.mainSetOption(key: 'user_info', value: value); + userName.value = info['name']; } return ''; } @@ -74,10 +75,12 @@ class UserModel { } final userInfo = await bind.mainGetLocalOption(key: 'user_info'); if (userInfo.trim().isEmpty) { - return ""; + return ''; } final m = jsonDecode(userInfo); - if (m != null) { + if (m == null) { + userName.value = ''; + } else { userName.value = m['name'] ?? ''; } return userName.value; @@ -86,10 +89,10 @@ class UserModel { Future logOut() async { final tag = gFFI.dialogManager.showLoading(translate('Waiting')); final url = await bind.mainGetApiServer(); - final _ = await http.post(Uri.parse("$url/api/logout"), + final _ = await http.post(Uri.parse('$url/api/logout'), body: { - "id": await bind.mainGetMyId(), - "uuid": await bind.mainGetUuid(), + 'id': await bind.mainGetMyId(), + 'uuid': await bind.mainGetUuid(), }, headers: await getHttpHeaders()); await Future.wait([ @@ -98,30 +101,30 @@ class UserModel { bind.mainSetLocalOption(key: 'selected-tags', value: ''), ]); parent.target?.abModel.clear(); - userName.value = ""; + 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"}, + 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() + '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'] ?? ""); + key: 'access_token', value: body['access_token'] ?? ''); bind.mainSetLocalOption( - key: "user_info", value: jsonEncode(body['user'])); - this.userName.value = body['user']?['name'] ?? ""; + key: 'user_info', value: jsonEncode(body['user'])); + this.userName.value = body['user']?['name'] ?? ''; return body; } catch (err) { - return {"error": "$err"}; + return {'error': '$err'}; } } } diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index aaa02c327..34983599a 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -564,6 +564,7 @@ message Misc { bool restart_remote_device = 14; bool uac = 15; bool foreground_window_elevated = 16; + bool stop_service = 17; } } diff --git a/res/PKGBUILD b/res/PKGBUILD index 7c97d419e..cff61516f 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -7,7 +7,7 @@ arch=('x86_64') url="" license=('AGPL-3.0') groups=() -depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'pipewire' 'curl' 'libva' 'libvdpau' 'libayatana-appindicator') +depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'curl' 'libva' 'libvdpau' 'libappindicator-gtk3') makedepends=() checkdepends=() optdepends=() diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec new file mode 100644 index 000000000..a01926baa --- /dev/null +++ b/res/rpm-flutter.spec @@ -0,0 +1,88 @@ +Name: rustdesk +Version: 1.2.0 +Release: 0 +Summary: RPM package +License: GPL-3.0 +Requires: gtk3 libxcb libxdo libXfixes pipewire alsa-lib curl libappindicator libvdpau1 libva2 + + +%description +The best open-source remote desktop client software, written in Rust. + +%prep +# we have no source, so nothing here + +%build +# we have no source, so nothing here + +# %global __python %{__python3} + +%install + +mkdir -p "${buildroot}/usr/lib/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "${buildroot}/usr/lib/rustdesk" +mkdir -p "${buildroot}/usr/bin" +pushd ${buildroot} && ln -s /usr/lib/rustdesk/rustdesk usr/bin/rustdesk && popd +install -Dm 644 $HBB/res/rustdesk.service -t "${buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk.desktop -t "${buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk-link.desktop -t "${buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/128x128@2x.png "${buildroot}/usr/share/rustdesk/files/rustdesk.png" + + +%files +/usr/bin/rustdesk +/usr/lib/rustdesk/* +/usr/share/rustdesk/files/rustdesk.service +/usr/share/rustdesk/files/rustdesk.png +/usr/share/rustdesk/files/rustdesk.desktop +/usr/share/rustdesk/files/rustdesk-link.desktop + +%changelog +# let's skip this for now + +# https://www.cnblogs.com/xingmuxin/p/8990255.html +%pre +# can do something for centos7 +case "$1" in + 1) + # for install + ;; + 2) + # for upgrade + systemctl stop rustdesk || true + ;; +esac + +%post +cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service +cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ +cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ +systemctl daemon-reload +systemctl enable rustdesk +systemctl start rustdesk +update-desktop-database + +%preun +case "$1" in + 0) + # for uninstall + systemctl stop rustdesk || true + systemctl disable rustdesk || true + rm /etc/systemd/system/rustdesk.service || true + ;; + 1) + # for upgrade + ;; +esac + +%postun +case "$1" in + 0) + # for uninstall + rm /usr/share/applications/rustdesk.desktop || true + rm /usr/share/applications/rustdesk-link.desktop || true + update-desktop-database + ;; + 1) + # for upgrade + ;; +esac diff --git a/res/rpm-suse.spec b/res/rpm-suse.spec index c328f42aa..5d03d9c8a 100644 --- a/res/rpm-suse.spec +++ b/res/rpm-suse.spec @@ -3,7 +3,7 @@ Version: 1.1.9 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb1 xdotool libXfixes3 pipewire alsa-utils curl libXtst6 libayatana-appindicator3-1 libvdpau1 libva2 +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libayatana-appindicator3-1 libvdpau1 libva2 %description The best open-source remote desktop client software, written in Rust. diff --git a/res/rpm.spec b/res/rpm.spec index e88fe1fda..b2a3e27e1 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,9 +1,9 @@ Name: rustdesk -Version: 1.1.9 +Version: 1.2.0 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes pipewire alsa-lib curl libayatana-appindicator3-1 libvdpau1 libva2 +Requires: gtk3 libxcb libxdo libXfixes alsa-lib curl libappindicator libvdpau1 libva2 %description The best open-source remote desktop client software, written in Rust. diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f20eb6153..9552bd36e 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -13,6 +13,8 @@ use hbb_common::{ fs, log, }; +// use crate::hbbs_http::account::AuthResult; + use crate::flutter::{self, SESSIONS}; #[cfg(target_os = "android")] use crate::start_server; @@ -1082,6 +1084,20 @@ pub fn install_install_path() -> SyncReturn { SyncReturn(install_path()) } +pub fn main_account_auth(op: String) { + let id = get_id(); + let uuid = get_uuid(); + account_auth(op, id, uuid); +} + +pub fn main_account_auth_cancel() { + account_auth_cancel() +} + +pub fn main_account_auth_result() -> String { + account_auth_result() +} + #[cfg(target_os = "android")] pub mod server_side { use jni::{ diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs new file mode 100644 index 000000000..ceb3a6081 --- /dev/null +++ b/src/hbbs_http.rs @@ -0,0 +1,34 @@ +use reqwest::blocking::Response; +use serde::de::DeserializeOwned; +use serde_json::{Map, Value}; + +#[cfg(feature = "flutter")] +pub mod account; + +#[derive(Debug)] +pub enum HbbHttpResponse { + ErrorFormat, + Error(String), + DataTypeFormat, + Data(T), +} + +impl TryFrom for HbbHttpResponse { + type Error = reqwest::Error; + + fn try_from(resp: Response) -> Result>::Error> { + let map = resp.json::>()?; + if let Some(error) = map.get("error") { + if let Some(err) = error.as_str() { + Ok(Self::Error(err.to_owned())) + } else { + Ok(Self::ErrorFormat) + } + } else { + match serde_json::from_value(Value::Object(map)) { + Ok(v) => Ok(Self::Data(v)), + Err(_) => Ok(Self::DataTypeFormat), + } + } + } +} diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs new file mode 100644 index 000000000..cdf724971 --- /dev/null +++ b/src/hbbs_http/account.rs @@ -0,0 +1,255 @@ +use super::HbbHttpResponse; +use hbb_common::{ + config::{Config, LocalConfig}, + log, sleep, tokio, ResultType, +}; +use reqwest::blocking::Client; +use serde_derive::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, + time::{Duration, Instant}, +}; +use url::Url; + +lazy_static::lazy_static! { + static ref API_SERVER: String = crate::get_api_server( + Config::get_option("api-server"), Config::get_option("custom-rendezvous-server")); + static ref OIDC_SESSION: Arc> = Arc::new(RwLock::new(OidcSession::new())); +} + +const QUERY_INTERVAL_SECS: f32 = 1.0; +const QUERY_TIMEOUT_SECS: u64 = 60 * 3; +const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth"; +const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth"; +const LOGIN_ACCOUNT_AUTH: &str = "Login account auth"; + +#[derive(Deserialize, Clone, Debug)] +pub struct OidcAuthUrl { + code: String, + url: Url, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct UserPayload { + pub id: String, + pub name: String, + pub email: Option, + pub note: Option, + pub status: Option, + pub grp: Option, + pub is_admin: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthBody { + pub access_token: String, + pub token_type: String, + pub user: UserPayload, +} + +pub struct OidcSession { + client: Client, + state_msg: &'static str, + failed_msg: String, + code_url: Option, + auth_body: Option, + keep_querying: bool, + running: bool, + query_timeout: Duration, +} + +#[derive(Serialize)] +pub struct AuthResult { + pub state_msg: String, + pub failed_msg: String, + pub url: Option, + pub auth_body: Option, +} + +impl OidcSession { + fn new() -> Self { + Self { + client: Client::new(), + state_msg: REQUESTING_ACCOUNT_AUTH, + failed_msg: "".to_owned(), + code_url: None, + auth_body: None, + keep_querying: false, + running: false, + query_timeout: Duration::from_secs(QUERY_TIMEOUT_SECS), + } + } + + fn auth(op: &str, id: &str, uuid: &str) -> ResultType> { + Ok(OIDC_SESSION + .read() + .unwrap() + .client + .post(format!("{}/api/oidc/auth", *API_SERVER)) + .json(&HashMap::from([("op", op), ("id", id), ("uuid", uuid)])) + .send()? + .try_into()?) + } + + fn query(code: &str, id: &str, uuid: &str) -> ResultType> { + let url = reqwest::Url::parse_with_params( + &format!("{}/api/oidc/auth-query", *API_SERVER), + &[("code", code), ("id", id), ("uuid", uuid)], + )?; + Ok(OIDC_SESSION + .read() + .unwrap() + .client + .get(url) + .send()? + .try_into()?) + } + + fn reset(&mut self) { + self.state_msg = REQUESTING_ACCOUNT_AUTH; + self.failed_msg = "".to_owned(); + self.keep_querying = true; + self.running = false; + self.code_url = None; + self.auth_body = None; + } + + fn before_task(&mut self) { + self.reset(); + self.running = true; + } + + fn after_task(&mut self) { + self.running = false; + } + + fn sleep(secs: f32) { + std::thread::sleep(std::time::Duration::from_secs_f32(secs)); + } + + fn auth_task(op: String, id: String, uuid: String) { + let auth_request_res = Self::auth(&op, &id, &uuid); + log::info!("Request oidc auth result: {:?}", &auth_request_res); + let code_url = match auth_request_res { + Ok(HbbHttpResponse::<_>::Data(code_url)) => code_url, + Ok(HbbHttpResponse::<_>::Error(err)) => { + OIDC_SESSION + .write() + .unwrap() + .set_state(REQUESTING_ACCOUNT_AUTH, err); + return; + } + Ok(_) => { + OIDC_SESSION + .write() + .unwrap() + .set_state(REQUESTING_ACCOUNT_AUTH, "Invalid auth response".to_owned()); + return; + } + Err(err) => { + OIDC_SESSION + .write() + .unwrap() + .set_state(REQUESTING_ACCOUNT_AUTH, err.to_string()); + return; + } + }; + + OIDC_SESSION + .write() + .unwrap() + .set_state(WAITING_ACCOUNT_AUTH, "".to_owned()); + OIDC_SESSION.write().unwrap().code_url = Some(code_url.clone()); + + let begin = Instant::now(); + let query_timeout = OIDC_SESSION.read().unwrap().query_timeout; + while OIDC_SESSION.read().unwrap().keep_querying && begin.elapsed() < query_timeout { + match Self::query(&code_url.code, &id, &uuid) { + Ok(HbbHttpResponse::<_>::Data(auth_body)) => { + LocalConfig::set_option( + "access_token".to_owned(), + auth_body.access_token.clone(), + ); + LocalConfig::set_option( + "user_info".to_owned(), + serde_json::to_string(&auth_body.user).unwrap_or_default(), + ); + OIDC_SESSION + .write() + .unwrap() + .set_state(LOGIN_ACCOUNT_AUTH, "".to_owned()); + OIDC_SESSION.write().unwrap().auth_body = Some(auth_body); + return; + } + Ok(HbbHttpResponse::<_>::Error(err)) => { + if err.contains("No authed oidc is found") { + // ignore, keep querying + } else { + OIDC_SESSION + .write() + .unwrap() + .set_state(WAITING_ACCOUNT_AUTH, err); + return; + } + } + Ok(_) => { + // ignore + } + Err(err) => { + log::trace!("Failed query oidc {}", err); + // ignore + } + } + Self::sleep(QUERY_INTERVAL_SECS); + } + + if begin.elapsed() >= query_timeout { + OIDC_SESSION + .write() + .unwrap() + .set_state(WAITING_ACCOUNT_AUTH, "timeout".to_owned()); + } + + // no need to handle "keep_querying == false" + } + + fn set_state(&mut self, state_msg: &'static str, failed_msg: String) { + self.state_msg = state_msg; + self.failed_msg = failed_msg; + } + + fn wait_stop_querying() { + let wait_secs = 0.3; + while OIDC_SESSION.read().unwrap().running { + Self::sleep(wait_secs); + } + } + + pub fn account_auth(op: String, id: String, uuid: String) { + Self::auth_cancel(); + Self::wait_stop_querying(); + OIDC_SESSION.write().unwrap().before_task(); + std::thread::spawn(|| { + Self::auth_task(op, id, uuid); + OIDC_SESSION.write().unwrap().after_task(); + }); + } + + fn get_result_(&self) -> AuthResult { + AuthResult { + state_msg: self.state_msg.to_string(), + failed_msg: self.failed_msg.clone(), + url: self.code_url.as_ref().map(|x| x.url.to_string()), + auth_body: self.auth_body.clone(), + } + } + + pub fn auth_cancel() { + OIDC_SESSION.write().unwrap().keep_querying = false; + } + + pub fn get_result() -> AuthResult { + OIDC_SESSION.read().unwrap().get_result_() + } +} diff --git a/src/lang.rs b/src/lang.rs index 25e7a3931..db59e9f54 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -23,6 +23,7 @@ mod tw; mod vn; mod kz; mod ua; +mod fa; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -49,6 +50,7 @@ lazy_static::lazy_static! { ("ko", "한국어"), ("kz", "Қазақ"), ("ua", "Українська"), + ("fa", "فارسی"), ]); } @@ -99,6 +101,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "ko" => ko::T.deref(), "kz" => kz::T.deref(), "ua" => ua::T.deref(), + "fa" => fa::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 3ee4735e3..7240f2a91 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"), ("Show RustDesk", "显示rustdesk"), ("This PC", "此电脑"), + ("or", "或"), + ("Continue with", "使用"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e0998a7bb..b51cb69e9 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte prosím obrazovku, kterou chcete sdílet (Ovládejte na straně protějšku)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 9aa4f00b9..c4d633b9b 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Vælg venligst den skærm, der skal deles (Betjen på peer-siden)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 4ce6cbd56..9eb90ebcd 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den Bildschirm aus, der freigegeben werden soll (auf der Peer-Seite arbeiten)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 72aa45853..3415fa463 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -35,5 +35,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_foreground_window_warning", "Temporarily unable to use the mouse and keyboard, because the current window of the remote desktop requires higher privilege to operate, you can request the remote user to minimize the current window. To avoid this problem, it is recommended to install the software on the remote device or run it with administrator privileges."), ("JumpLink", "View"), ("Stop service", "Stop Service"), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 8fb58cf83..e7a35d937 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Bonvolu Elekti la ekranon por esti dividita (Funkciu ĉe la sama flanko)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index fa6aba297..7582926f3 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Seleccione la pantalla que se compartirá (Operar en el lado del par)."), ("Show RustDesk", "Mostrar RustDesk"), ("This PC", "Este PC"), + ("or", "o"), + ("Continue with", "Continuar con"), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs new file mode 100644 index 000000000..f9a15dc9e --- /dev/null +++ b/src/lang/fa.rs @@ -0,0 +1,393 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "وضعیت"), + ("Your Desktop", "دسکتاپ شما"), + ("desk_tip", "دسکتاپ شما با این شناسه و رمز عبور قابل دسترسی است"), + ("Password", "رمز عبور"), + ("Ready", "آماده به کار"), + ("Established", "اتصال برقرار شد"), + ("connecting_status", "...در حال برقراری ارتباط با سرور"), + ("Enable Service", "فعالسازی سرویس"), + ("Start Service", "اجرا سرویس"), + ("Service is running", "سرویس در حال اجرا است"), + ("Service is not running", "سرویس اجرا نشده"), + ("not_ready_status", "ارتباط برقرار نشد. لطفا شبکه خود را بررسی کنید"), + ("Control Remote Desktop", "کنترل دسکتاپ میزبان"), + ("Transfer File", "جابه جایی فایل"), + ("Connect", "اتصال"), + ("Recent Sessions", "جلسات اخیر"), + ("Address Book", "دفترچه آدرس"), + ("Confirmation", "تایید"), + ("TCP Tunneling", "TCP تانل"), + ("Remove", "حذف"), + ("Refresh random password", "رمز عبور تصادفی را بروز کنید"), + ("Set your own password", "!رمز عبور دلخواه بگذارید"), + ("Enable Keyboard/Mouse", "Keyboard/Mouse فعالسازی"), + ("Enable Clipboard", "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", "خالی است Clipboard"), + ("Stop service", "توقف سرویس"), + ("Change ID", "تعویض شناسه"), + ("Website", "وب سایت"), + ("About", "درباره"), + ("Mute", "بستن صدا"), + ("Audio Input", "ورودی صدا"), + ("Enhancements", "بهبودها"), + ("Hardware Codec", "کدک سخت افزاری"), + ("Adaptive Bitrate", ""), + ("ID Server", "شناسه سرور"), + ("Relay Server", "Relay سرور"), + ("API Server", "API سرور"), + ("invalid_http", "شروع شود http:// یا https:// باید با"), + ("Invalid IP", "نامعتبر است IP آدرس"), + ("id_change_tip", "شناسه باید طبق این شرایط باشد : حروف کوچک و بزرگ انگلیسی و اعداد از 0 تا 9، _ و همچنین حرف اول آن فقط حروف بزرگ یا کوچک انگلیسی و طول آن بین 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", "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", "Clipboard غیرفعالسازی"), + ("Lock after session end", "قفل کردن حساب کاربری سیستم عامل پس از پایان جلسه"), + ("Insert", "افزودن"), + ("Insert Lock", "افزودن قفل"), + ("Refresh", "تازه سازی"), + ("ID does not exist", "شناسه وجود ندارد"), + ("Failed to connect to rendezvous server", "اتصال به سرور تولید شناسه انجام نشد"), + ("Please try later", "لطفا بعدا تلاش کنید"), + ("Remote desktop is offline", "دسکتاپ از راه دور خاموش است"), + ("Key mismatch", "عدم تطابق کلید"), + ("Timeout", "زمان انتظار به پایان رسید"), + ("Failed to connect to relay server", "سرور وصل نشد Relay به"), + ("Failed to connect via rendezvous server", "اتصال از طریق سرور تولید شناسه انجام نشد"), + ("Failed to connect via relay server", "انجام نشد Relay اتصال از طریق سرور"), + ("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 ...", "در حال نصب..."), + ("Install", "نصب"), + ("Installation", "نصب و راه اندازی"), + ("Installation Path", "محل نصب"), + ("Create start menu shortcuts", "Start ایجاد میانبرها در منوی"), + ("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", "را بدهید Clipboard اجازه استفاده از"), + ("Allow hearing sound", "اجازه شنیدن صدا را بدهید"), + ("Allow file copy and paste", "اجازه کپی و چسباندن فایل را بدهید"), + ("Connected", "متصل شده"), + ("Direct and encrypted connection", "اتصال مستقیم و رمزگذاری شده"), + ("Relayed and encrypted connection", "و رمزگذاری شده Relay اتصال از طریق"), + ("Direct and unencrypted connection", "اتصال مستقیم و بدون رمزگذاری"), + ("Relayed and unencrypted connection", "و رمزگذاری نشده Relay اتصال از طریق"), + ("Enter Remote ID", "شناسه از راه دور را وارد کنید"), + ("Enter your password", "زمر عبور خود را وارد کنید"), + ("Logging in...", "در حال ورود..."), + ("Enable RDP session sharing", "اشتراک گذاری جلسه RDP را فعال کنید"), + ("Auto Login", "ورود خودکار"), + ("Enable Direct IP Access", "دسترسی مستقیم IP را فعال کنید"), + ("Rename", "تغییر نام"), + ("Space", "فضا"), + ("Create Desktop Shortcut", "ساخت میانبر روی دسکتاپ"), + ("Change Path", "تغییر مسیر"), + ("Create Folder", "ایجاد پوشه"), + ("Please enter the folder name", "نام پوشه را وارد کنید"), + ("Fix it", "بازسازی"), + ("Warning", "هشدار"), + ("Login screen using Wayland is not supported", "ورود به سیستم با استفاده از Wayland پشتیبانی نمی شود"), + ("Reboot required", "راه اندازی مجدد مورد نیاز است"), + ("Unsupported display server ", "سرور تصویر پشتیبانی نشده است"), + ("x11 expected", ""), + ("Port", "پورت"), + ("Settings", "تنظیمات"), + ("Username", "نام کاربری"), + ("Invalid port", "پورت نامعتبر است"), + ("Closed manually by the peer", "به صورت دستی توسط میزبان بسته شد"), + ("Enable remote configuration modification", "تغییرات پیکربندی از راه دور را مجاز کنید"), + ("Run without install", "بدون نصب اجرا شود"), + ("Always connected via relay", "متصل است Relay همیشه با"), + ("Always connect via relay", "برای اتصال استفاده کنید Relay از"), + ("whitelist_tip", "فقط آدرس های IP مجاز می توانند به این دسکتاپ متصل شوند"), + ("Login", "ورود"), + ("Logout", "خروج"), + ("Tags", "برچسب ها"), + ("Search ID", "جستجوی شناسه"), + ("Current Wayland display server is not supported", "سرور نمای فعلی Wayland پشتیبانی نمی شود"), + ("whitelist_sep", "با کاما، نقطه ویرگول، فاصله یا خط جدید از هم جدا می شوند"), + ("Add 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 Proxy"), + ("Hostname", "Hostname"), + ("Discovered", "پیدا شده"), + ("install_daemon_tip", "برای شروع در هنگام راه اندازی، باید سرویس سیستم را نصب کنید"), + ("Remote ID", "شناسه از راه دور"), + ("Paste", "درج کنید"), + ("Paste here?", "اینجا درج شود؟"), + ("Are you sure to close the connection?", "آیا مطمئن هستید که می خواهید اتصال را پایان دهید؟"), + ("Download new version", "دانلود نسخه جدید"), + ("Touch mode", "حالت لمسی"), + ("Mouse mode", "حالت ماوس"), + ("One-Finger Tap", "با یک انگشت لمس کنید"), + ("Left Mouse", "دکمه سمت چپ ماوس"), + ("One-Long Tap", "لمس طولانی با یک انگشت"), + ("Two-Finger Tap", "با دو انگشت لمس کنید"), + ("Right Mouse", "دکمه سمت راست ماوس"), + ("One-Finger Move", "با یک انگشت حرکت کنید"), + ("Double Tap & Move", "دو ضربه سریع بزنید و حرکت دهید"), + ("Mouse Drag", "کشیدن ماوس"), + ("Three-Finger vertically", "سه انگشت عمودی"), + ("Mouse Wheel", "چرخ ماوس"), + ("Two-Finger Move", "با دو انگشت حرکت کنید"), + ("Canvas Move", ""), + ("Pinch to Zoom", "زوم را کوچک کنید"), + ("Canvas Zoom", ""), + ("Reset canvas", ""), + ("No permission of file transfer", "مجوز انتقال فایل داده نشده"), + ("Note", "یادداشت"), + ("Connection", "ارتباط"), + ("Share Screen", "اشتراک گذاری صفحه"), + ("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", "برای اینکه یک دستگاه راه دور بتواند دستگاه Android شما را از طریق ماوس یا لمسی کنترل کند، باید به RustDesk اجازه دهید از ویژگی \"Accessibility\" استفاده کند."), + ("android_input_permission_tip2", "به صفحه تنظیمات سیستم زیر بروید، \"Installed Services\" را پیدا کرده و وارد کنید، سرویس \"RustDesk Input\" را فعال کنید"), + ("android_new_connection_tip", "درخواست جدیدی برای مدیریت دستگاه فعلی شما دریافت شده است."), + ("android_service_will_start_tip", "فعال کردن ضبط صفحه به طور خودکار سرویس را راه اندازی می کند و به دستگاه های دیگر امکان می دهد درخواست اتصال به آن دستگاه را داشته باشند."), + ("android_stop_service_tip", "با بستن سرویس، تمام اتصالات برقرار شده به طور خودکار بسته می شود"), + ("android_version_audio_tip", "نسخه فعلی اندروید از ضبط صدا پشتیبانی نمی‌کند، لطفاً به اندروید 10 یا بالاتر به‌روزرسانی کنید"), + ("android_start_service_tip", "برای شروع سرویس اشتراک‌گذاری صفحه، روی مجوز \"شروع مرحله‌بندی سرور\" یا OPEN \"Screen Capture\" کلیک کنید."), + ("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", "بهینه سازی باتری را نادیده بگیرید"), + ("android_open_battery_optimizations_tip", "به صفحه تنظیمات بعدی بروید"), + ("Connection not allowed", "اتصال مجاز نیست"), + ("Legacy mode", "پشتیبانی موارد قدیمی"), + ("Map mode", "حالت نقشه"), + ("Translate mode", "حالت ترجمه"), + ("Use temporary password", "از رمز عبور موقت استفاده کنید"), + ("Use permanent password", "از رمز عبور دائمی استفاده کنید"), + ("Use both passwords", "از هر دو رمز عبور استفاده کنید"), + ("Set permanent password", "یک رمز عبور دائمی تنظیم کنید"), + ("Set temporary password length", "تنظیم طول رمز عبور موقت"), + ("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", "Relay ارتباط"), + ("Secure Connection", "ارتباط امن"), + ("Insecure Connection", "ارتباط غیر امن"), + ("Scale original", "مقیاس اصلی"), + ("Scale adaptive", "مقیاس تطبیقی"), + ("General", "عمومی"), + ("Security", "امنیت"), + ("Account", "حساب کاربری"), + ("Theme", "نمایه"), + ("Dark Theme", "نمایه تیره"), + ("Dark", "تیره"), + ("Light", "روشن"), + ("Follow System", "سیستم را دنبال کنید"), + ("Enable hardware codec", "از کدک سخت افزاری استفاده کنید"), + ("Unlock Security Settings", "تنظیمات امنیتی را باز کنید"), + ("Enable Audio", "صدا را روشن کنید"), + ("Temporary Password Length", "طول رمز عبور موقت"), + ("Unlock Network Settings", "باز کردن قفل تنظیمات شبکه"), + ("Server", "سرور"), + ("Direct IP Access", "دسترسی مستقیم به IP"), + ("Proxy", "پروکسی"), + ("Port", "پورت"), + ("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", "فعالسازی جستجو در شبکه"), + ("Deny LAN Discovery", "غیر فعالسازی جستجو در شبکه"), + ("Write a message", "یک پیام بنویسید"), + ("Prompt", ""), + ("elevation_prompt", "اجرای نرم‌افزار بدون افزایش امتیاز می‌تواند باعث ایجاد مشکلاتی در هنگام کار کردن کاربران راه دور با ویندوزهای خاص شود"), + ("uac_warning", "به دلیل درخواست دسترسی سطح بالا، به طور موقت از دسترسی رد شد. منتظر بمانید تا کاربر راه دور گفتگوی UAC را بپذیرد. برای جلوگیری از این مشکل، توصیه می شود نرم افزار را روی دستگاه از راه دور نصب کنید یا آن را با دسترسی مدیر اجرا کنید."), + ("elevated_foreground_window_warning", "به طور موقت استفاده از ماوس و صفحه کلید امکان پذیر نیست زیرا پنجره دسکتاپ از راه دور فعلی برای کار کردن به دسترسی های بالاتر نیاز دارد، می توانید از کاربر راه دور بخواهید که پنجره فعلی را به حداقل برساند. برای جلوگیری از این مشکل، توصیه می شود نرم افزار را روی یک دستگاه راه دور نصب کنید یا آن را با دسترسی مدیر اجرا کنید"), + ("Disconnected", "قطع ارتباط"), + ("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.", ""), + ("JumpLink", ""), + ("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحه‌ای را برای اشتراک‌گذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."), + ("Show RustDesk", "RustDesk را نشان دهید"), + ("This PC", "This PC"), + ("or", "یا"), + ("Continue with", "ادامه با"), + ].iter().cloned().collect(); +} diff --git a/src/lang/fr.rs b/src/lang/fr.rs index f1119e166..a64fd6028 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l'écran à partager (opérer du côté pair)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 262574c43..c449c393d 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Kérjük, válassza ki a megosztani kívánt képernyőt (a társoldalon működjön)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 2d2ab9b1a..6f328f127 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 6965e6610..75e7859ed 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Seleziona lo schermo da condividere (opera sul lato peer)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 12dc9ebaf..0e6931379 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "共有する画面を選択してください(ピア側で操作)。"), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 406de6cef..601db354d 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하십시오(피어 측에서 작동)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index df1237bfb..359e14f55 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Бөлісетін экранды таңдаңыз (бірдей жағынан жұмыс жасаңыз)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 201a60811..382b254f0 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po stronie równorzędnej)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index a9189fc14..b99cb9db0 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Por favor, selecione a tela a ser compartilhada (operar no lado do peer)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index d6a7ccec0..3a60dc313 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -2,8 +2,8 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), - ("Your Desktop", "Seu Desktop"), - ("desk_tip", "Seu desktop pode ser acessado com este ID e senha."), + ("Your Desktop", "Seu Computador"), + ("desk_tip", "Seu computador pode ser acessado com este ID e senha."), ("Password", "Senha"), ("Ready", "Pronto"), ("Established", "Estabelecido"), @@ -13,37 +13,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Serviço está em execução"), ("Service is not running", "Serviço não está em execução"), ("not_ready_status", "Não está pronto. Por favor verifique sua conexão"), - ("Control Remote Desktop", "Controle o Desktop à distância"), + ("Control Remote Desktop", "Controle um Computador Remoto"), ("Transfer File", "Transferir Arquivo"), ("Connect", "Conectar"), - ("Recent Sessions", "Sessões recentes"), + ("Recent Sessions", "Sessões Recentes"), ("Address Book", "Lista de Endereços"), ("Confirmation", "Confirmação"), ("TCP Tunneling", "Tunelamento TCP"), ("Remove", "Remover"), ("Refresh random password", "Atualizar senha aleatória"), ("Set your own password", "Configure sua própria senha"), - ("Enable Keyboard/Mouse", "Habilitar Teclado/Mouse"), + ("Enable Keyboard/Mouse", "Habilitar teclado/mouse"), ("Enable Clipboard", "Habilitar Área de Transferência"), ("Enable File Transfer", "Habilitar Transferência de Arquivos"), ("Enable TCP Tunneling", "Habilitar Tunelamento TCP"), - ("IP Whitelisting", "Whitelist de IP"), + ("IP Whitelisting", "Lista de IPs Confiáveis"), ("ID/Relay Server", "Servidor ID/Relay"), ("Import Server Config", "Importar Configuração do Servidor"), - ("Export Server Config", ""), + ("Export Server Config", "Exportar Configuração do Servidor"), ("Import server configuration successfully", "Configuração do servidor importada com sucesso"), - ("Export server configuration successfully", ""), + ("Export server configuration successfully", "Configuração do servidor exportada com sucesso"), ("Invalid server configuration", "Configuração do servidor inválida"), ("Clipboard is empty", "A área de transferência está vazia"), ("Stop service", "Parar serviço"), ("Change ID", "Alterar ID"), ("Website", "Website"), ("About", "Sobre"), - ("Mute", "Emudecer"), + ("Mute", "Desativar som"), ("Audio Input", "Entrada de Áudio"), - ("Enhancements", ""), - ("Hardware Codec", ""), - ("Adaptive Bitrate", ""), + ("Enhancements", "Melhorias"), + ("Hardware Codec", "Codec de hardware"), + ("Adaptive Bitrate", "Taxa de bits adaptável"), ("ID Server", "Servidor de ID"), ("Relay Server", "Servidor de Relay"), ("API Server", "Servidor da API"), @@ -59,18 +59,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Close", "Fechar"), ("Retry", "Tentar novamente"), ("OK", "OK"), - ("Password Required", "Senha Necessária"), + ("Password Required", "Senha necessária"), ("Please enter your password", "Por favor informe sua senha"), ("Remember password", "Lembrar senha"), - ("Wrong Password", "Senha Incorreta"), - ("Do you want to enter again?", "Você quer entrar novamente?"), - ("Connection Error", "Erro de Conexão"), + ("Wrong Password", "Senha incorreta"), + ("Do you want to enter again?", "Você deseja conectar novamente?"), + ("Connection Error", "Erro de conexão"), ("Error", "Erro"), - ("Reset by the peer", "Reiniciado pelo par"), + ("Reset by the peer", "Reiniciado pelo parceiro"), ("Connecting...", "Conectando..."), ("Connection in progress. Please wait.", "Conexão em progresso. Aguarde por favor."), ("Please try 1 minute later", "Por favor tente após 1 minuto"), - ("Login Error", "Erro de Login"), + ("Login Error", "Erro de login"), ("Successful", "Sucesso"), ("Connected, waiting for image...", "Conectado. Aguardando pela imagem..."), ("Name", "Nome"), @@ -88,10 +88,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Confirm Delete", "Confirmar Apagar"), ("Delete", "Apagar"), ("Properties", "Propriedades"), - ("Multi Select", "Seleção Múltipla"), - ("Select All", ""), - ("Unselect All", ""), - ("Empty Directory", "Diretório Vazio"), + ("Multi Select", "Seleção múltipla"), + ("Select All", "Selecionar tudo"), + ("Unselect All", "Desmarcar tudo"), + ("Empty Directory", "Diretório vazio"), ("Not an empty directory", "Diretório não está vazio"), ("Are you sure you want to delete this file?", "Tem certeza que deseja apagar este arquivo?"), ("Are you sure you want to delete this empty directory?", "Tem certeza que deseja apagar este diretório vazio?"), @@ -116,18 +116,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Qualidade visual boa"), ("Balanced", "Balanceada"), ("Optimize reaction time", "Otimizar tempo de reação"), - ("Custom", ""), + ("Custom", "Personalizado"), ("Show remote cursor", "Mostrar cursor remoto"), - ("Show quality monitor", ""), + ("Show quality monitor", "Exibir monitor de qualidade"), ("Disable clipboard", "Desabilitar área de transferência"), ("Lock after session end", "Bloquear após o fim da sessão"), ("Insert", "Inserir"), - ("Insert Lock", "Inserir Trava"), + ("Insert Lock", "Bloquear computador"), ("Refresh", "Atualizar"), ("ID does not exist", "ID não existe"), ("Failed to connect to rendezvous server", "Falha ao conectar ao servidor de rendezvous"), ("Please try later", "Por favor tente mais tarde"), - ("Remote desktop is offline", "Desktop remoto está offline"), + ("Remote desktop is offline", "O computador remoto está offline"), ("Key mismatch", "Chaves incompatíveis"), ("Timeout", "Tempo esgotado"), ("Failed to connect to relay server", "Falha ao conectar ao servidor de relay"), @@ -141,14 +141,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to download", "Clique para baixar"), ("Click to update", "Clique para fazer o update"), ("Configure", "Configurar"), - ("config_acc", "Para controlar seu Desktop remotamente, você precisa conceder ao RustDesk permissões de \"Acessibilidade\"."), - ("config_screen", "Para acessar seu Desktop remotamente, você precisa conceder ao RustDesk permissões de \"Gravar a Tela\"/"), + ("config_acc", "Para controlar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Acessibilidade\"."), + ("config_screen", "Para acessar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Gravar a Tela\"/"), ("Installing ...", "Instalando ..."), ("Install", "Instalar"), ("Installation", "Instalação"), ("Installation Path", "Caminho da Instalação"), - ("Create start menu shortcuts", "Criar atalhos no menu iniciar"), - ("Create desktop icon", "Criar ícone na área de trabalho"), + ("Create start menu shortcuts", "Criar atalhos no Menu Iniciar"), + ("Create desktop icon", "Criar ícone na Área de Trabalho"), ("agreement_tip", "Ao iniciar a instalação, você concorda com o acordo de licença."), ("Accept and Install", "Aceitar e Instalar"), ("End-user license agreement", "Acordo de licença do usuário final"), @@ -161,8 +161,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Action", "Ação"), ("Add", "Adicionar"), ("Local Port", "Porta Local"), - ("Local Address", ""), - ("Change Local Port", ""), + ("Local Address", "Endereço Local"), + ("Change Local Port", "Alterar Porta Local"), ("setup_server_tip", "Para uma conexão mais rápida, por favor configure seu próprio servidor"), ("Too short, at least 6 characters.", "Muito curto, pelo menos 6 caracteres."), ("The confirmation is not identical.", "A confirmação não é idêntica."), @@ -173,7 +173,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Allow using keyboard and mouse", "Permitir o uso de teclado e mouse"), ("Allow using clipboard", "Permitir o uso da área de transferência"), ("Allow hearing sound", "Permitir escutar som"), - ("Allow file copy and paste", "Permitir copiar e pegar arquivos"), + ("Allow file copy and paste", "Permitir copiar e colar arquivos"), ("Connected", "Conectado"), ("Direct and encrypted connection", "Conexão direta e criptografada"), ("Relayed and encrypted connection", "Conexão via relay e criptografada"), @@ -186,39 +186,39 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto Login", "Login Automático (Somente válido se você habilitou \"Bloquear após o fim da sessão\")"), ("Enable Direct IP Access", "Habilitar Acesso IP Direto"), ("Rename", "Renomear"), - ("Space", "Espaõ"), + ("Space", "Espaço"), ("Create Desktop Shortcut", "Criar Atalho na Área de Trabalho"), ("Change Path", "Alterar Caminho"), ("Create Folder", "Criar Diretório"), ("Please enter the folder name", "Por favor informe o nome do diretório"), - ("Fix it", "Conserte"), - ("Warning", "Aguardando"), + ("Fix it", "Corrigir"), + ("Warning", "Aviso"), ("Login screen using Wayland is not supported", "Tela de Login utilizando Wayland não é suportada"), ("Reboot required", "Reinicialização necessária"), ("Unsupported display server ", "Servidor de display não suportado"), ("x11 expected", "x11 esperado"), - ("Port", ""), + ("Port", "Porta"), ("Settings", "Configurações"), ("Username", "Nome de usuário"), ("Invalid port", "Porta inválida"), - ("Closed manually by the peer", "Fechada manualmente pelo par"), + ("Closed manually by the peer", "Fechada manualmente pelo parceiro"), ("Enable remote configuration modification", "Habilitar modificações de configuração remotas"), ("Run without install", "Executar sem instalar"), ("Always connected via relay", "Sempre conectado via relay"), ("Always connect via relay", "Sempre conectar via relay"), - ("whitelist_tip", "Somente IPs na whitelist podem me acessar"), + ("whitelist_tip", "Somente IPs confiáveis podem me acessar"), ("Login", "Login"), ("Logout", "Sair"), ("Tags", "Tags"), - ("Search ID", "Buscar ID"), + ("Search ID", "Pesquisar ID"), ("Current Wayland display server is not supported", "Servidor de display Wayland atual não é suportado"), ("whitelist_sep", "Separado por vírcula, ponto-e-vírgula, espaços ou nova linha"), ("Add ID", "Adicionar ID"), ("Add Tag", "Adicionar Tag"), - ("Unselect all tags", "Desselecionar todas as tags"), + ("Unselect all tags", "Desmarcar todas as tags"), ("Network error", "Erro de rede"), - ("Username missed", "Nome de usuário faltante"), - ("Password missed", "Senha faltante"), + ("Username missed", "Nome de usuário requerido"), + ("Password missed", "Senha requerida"), ("Wrong credentials", "Nome de usuário ou senha incorretos"), ("Edit Tag", "Editar Tag"), ("Unremember Password", "Esquecer Senha"), @@ -250,10 +250,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Mouse Wheel", "Roda do Mouse"), ("Two-Finger Move", "Mover com dois dedos"), ("Canvas Move", "Mover Tela"), - ("Pinch to Zoom", "Beliscar para Zoom"), - ("Canvas Zoom", "Zoom na Tela"), + ("Pinch to Zoom", "Pinçar para Zoom"), + ("Canvas Zoom", "Zoom na tela"), ("Reset canvas", "Reiniciar tela"), - ("No permission of file transfer", "Sem permissões de transferência de arquivo"), + ("No permission of file transfer", "Sem permissão para transferência de arquivo"), ("Note", "Nota"), ("Connection", "Conexão"), ("Share Screen", "Compartilhar Tela"), @@ -276,116 +276,118 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_new_connection_tip", "Nova requisição de controle recebida, solicita o controle de seu dispositivo atual."), ("android_service_will_start_tip", "Habilitar a Captura de Tela irá automaticamente inicalizar o serviço, permitindo que outros dispositivos solicitem uma conexão deste dispositivo."), ("android_stop_service_tip", "Fechar o serviço irá automaticamente fechar todas as conexões estabelecidas."), - ("android_version_audio_tip", "A versão atual do Android não suporta captura de áudio, por favor atualize para o Android 10 ou maior."), + ("android_version_audio_tip", "A versão atual do Android não suporta captura de áudio, por favor atualize para o Android 10 ou superior."), ("android_start_service_tip", "Toque [Iniciar Serviço] ou abra a permissão [Captura de Tela] para iniciar o serviço de compartilhamento de tela."), - ("Account", ""), + ("Account", "Conta"), ("Overwrite", "Substituir"), ("This file exists, skip or overwrite this file?", "Este arquivo existe, pular ou substituir este arquivo?"), - ("Quit", "Saída"), + ("Quit", "Sair"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "Ajuda"), ("Failed", "Falhou"), - ("Succeeded", "Conseguiu"), - ("Someone turns on privacy mode, exit", "Alguém liga o modo de privacidade, saia"), - ("Unsupported", "Sem suporte"), - ("Peer denied", "Par negado"), + ("Succeeded", "Sucesso"), + ("Someone turns on privacy mode, exit", "Alguém habilitou o modo de privacidade, sair"), + ("Unsupported", "Não suportado"), + ("Peer denied", "Parceiro negou"), ("Please install plugins", "Por favor instale plugins"), - ("Peer exit", "Saída de pares"), + ("Peer exit", "Parceiro saiu"), ("Failed to turn off", "Falha ao desligar"), ("Turned off", "Desligado"), ("In privacy mode", "No modo de privacidade"), ("Out privacy mode", "Fora do modo de privacidade"), - ("Language", ""), - ("Keep RustDesk background service", ""), - ("Ignore Battery Optimizations", ""), - ("android_open_battery_optimizations_tip", ""), - ("Connection not allowed", ""), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), - ("Use temporary password", ""), - ("Use permanent password", ""), - ("Use both passwords", ""), - ("Set permanent password", ""), - ("Set temporary password length", ""), - ("Enable Remote Restart", ""), - ("Allow remote restart", ""), - ("Restart Remote Device", ""), - ("Are you sure you want to restart", ""), - ("Restarting Remote Device", ""), + ("Language", "Idioma"), + ("Keep RustDesk background service", "Manter o serviço do RustDesk executando em segundo plano"), + ("Ignore Battery Optimizations", "Ignorar otimizações de bateria"), + ("android_open_battery_optimizations_tip", "Abrir otimizações de bateria"), + ("Connection not allowed", "Conexão não permitida"), + ("Legacy mode", "Modo legado"), + ("Map mode", "Modo mapa"), + ("Translate mode", "Modo traduzido"), + ("Use temporary password", "Utilizar senha temporária"), + ("Use permanent password", "Utilizar senha permanente"), + ("Use both passwords", "Utilizar ambas as senhas"), + ("Set permanent password", "Configurar senha permanente"), + ("Set temporary password length", "Configurar extensão da senha temporária"), + ("Enable Remote Restart", "Habilitar reinicialização remota"), + ("Allow remote restart", "Permitir reinicialização remota"), + ("Restart Remote Device", "Reiniciar dispositivo remoto"), + ("Are you sure you want to restart", "Você tem certeza que deseja reiniciar?"), + ("Restarting Remote Device", "Reiniciando dispositivo remoto"), ("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", ""), - ("Account", ""), - ("Theme", ""), - ("Dark Theme", ""), - ("Dark", ""), - ("Light", ""), - ("Follow System", ""), - ("Enable hardware codec", ""), - ("Unlock Security Settings", ""), - ("Enable Audio", ""), - ("Temporary Password Length", ""), - ("Unlock Network Settings", ""), - ("Server", ""), - ("Direct IP Access", ""), - ("Proxy", ""), - ("Port", ""), - ("Apply", ""), - ("Disconnect all devices?", ""), - ("Clear", ""), - ("Audio Input Device", ""), - ("Deny remote access", ""), - ("Use IP Whitelisting", ""), - ("Network", ""), - ("Enable 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", ""), - ("Deny LAN Discovery", ""), - ("Write a message", ""), - ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), - ("Disconnected", ""), - ("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.", ""), - ("JumpLink", "View"), - ("Please Select the screen to be shared(Operate on the peer side).", ""), - ("Show RustDesk", ""), - ("This PC", ""), + ("Copied", "Copiado"), + ("Exit Fullscreen", "Sair da Tela Cheia"), + ("Fullscreen", "Tela Cheia"), + ("Mobile Actions", "Ações móveis"), + ("Select Monitor", "Selecionar monitor"), + ("Control Actions", "Controlar ações"), + ("Display Settings", "Configurações de exibição"), + ("Ratio", "Proporção"), + ("Image Quality", "Qualidade de imagem"), + ("Scroll Style", "Estilo de rolagem"), + ("Show Menubar", "Exibir barra de menu"), + ("Hide Menubar", "Ocultar barra de menu"), + ("Direct Connection", "Conexão direta"), + ("Relay Connection", "Conexão relay"), + ("Secure Connection", "Conexão segura"), + ("Insecure Connection", "Conexão insegura"), + ("Scale original", "Escala original"), + ("Scale adaptive", "Escala adaptada"), + ("General", "Geral"), + ("Security", "Segurança"), + ("Account", "Conta"), + ("Theme", "Tema"), + ("Dark Theme", "Tema escuro"), + ("Dark", "Escuro"), + ("Light", "Claro"), + ("Follow System", "Seguir sistema"), + ("Enable hardware codec", "Habilitar codec de hardware"), + ("Unlock Security Settings", "Desabilitar configurações de segurança"), + ("Enable Audio", "Habilitar áudio"), + ("Temporary Password Length", "Extensão da senha temporária"), + ("Unlock Network Settings", "Desbloquear configurações de rede"), + ("Server", "Servidor"), + ("Direct IP Access", "Acesso direto por IP"), + ("Proxy", "Proxy"), + ("Port", "Porta"), + ("Apply", "Aplicar"), + ("Disconnect all devices?", "Desconectar todos os dispositivos?"), + ("Clear", "Limpar"), + ("Audio Input Device", "Dispositivo de entrada de áudio"), + ("Deny remote access", "Negar acesso remoto"), + ("Use IP Whitelisting", "Utilizar lista de IPs confiáveis"), + ("Network", "Rede"), + ("Enable RDP", "Habilitar RDP"), + ("Pin menubar", "Fixar barra de menu"), + ("Unpin menubar", "Desafixar barra de menu"), + ("Recording", "Gravando"), + ("Directory", "Diretório"), + ("Automatically record incoming sessions", "Gravar automaticamente sessões de entrada"), + ("Change", "Alterar"), + ("Start session recording", "Iniciar gravação da sessão"), + ("Stop session recording", "Parar gravação da sessão"), + ("Enable Recording Session", "Habilitar gravação da sessão"), + ("Allow recording session", "Permitir gravação da sessão"), + ("Enable LAN Discovery", "Habilitar descoberta da LAN"), + ("Deny LAN Discovery", "Negar descoberta da LAN"), + ("Write a message", "Escrever uma mensagem"), + ("Prompt", "Prompt de comando"), + ("elevation_prompt", "Prompt de comando (Admin)"), + ("uac_warning", "Aviso UAC"), + ("elevated_foreground_window_warning", "Aviso de janela de primeiro plano elevado"), + ("Disconnected", "Desconectado"), + ("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."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requer uma versão superior da distribuição linux. Por favor, tente o desktop X11 ou mude seu sistema operacional."), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Por favor, selecione a tela a ser compartilhada (operar no lado do parceiro)."), + ("Show RustDesk", "Exibir RustDesk"), + ("This PC", "Este PC"), + ("or", "ou"), + ("Continue with", "Continuar com"), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index dfe1f7e8f..4eae0d153 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Пожалуйста, выберите экран для совместного использования (работайте на одноранговой стороне)."), ("Show RustDesk", "Показать RustDesk"), ("This PC", "Этот компьютер"), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 38196c011..4dfd8b02e 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte obrazovku, ktorú chcete zdieľať (Ovládajte na strane partnera)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index d5130d66b..7c1f18df3 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", ""), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index fe9b5d2fc..f856182f3 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Lütfen paylaşılacak ekranı seçiniz (Ekran tarafında çalıştırın)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0f986ffa1..6a196feb7 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的畫面(在對端操作)。"), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 1c34d0825..95d19b26b 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Будь ласка, виберіть екран, до якого потрібно надати доступ (працюйте на стороні однорангового пристрою)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 255b60def..c95498ca8 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -387,5 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please Select the screen to be shared(Operate on the peer side).", "Vui lòng Chọn màn hình để chia sẻ (Hoạt động ở phía ngang hàng)."), ("Show RustDesk", ""), ("This PC", ""), + ("or", ""), + ("Continue with", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index 58dc50b04..eb8a876ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,8 @@ mod ui_cm_interface; mod ui_interface; mod ui_session_interface; +mod hbbs_http; + #[cfg(windows)] pub mod clipboard_file; diff --git a/src/main.rs b/src/main.rs index ac8fd5219..9c7170309 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,8 +32,8 @@ fn main() { if !common::global_init() { return; } - use hbb_common::log; use clap::App; + use hbb_common::log; let args = format!( "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' -k, --key=[KEY] '' @@ -45,7 +45,7 @@ fn main() { .about("RustDesk command line tool") .args_from_usage(&args) .get_matches(); - use hbb_common::{env_logger::*, config::LocalConfig}; + use hbb_common::{config::LocalConfig, env_logger::*}; init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); if let Some(p) = matches.value_of("port-forward") { let options: Vec = p.split(":").map(|x| x.to_owned()).collect(); @@ -73,7 +73,14 @@ fn main() { } let key = matches.value_of("key").unwrap_or("").to_owned(); let token = LocalConfig::get_option("access_token"); - cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port, key, token); + cli::start_one_port_forward( + options[0].clone(), + port, + remote_host, + remote_port, + key, + token, + ); } common::global_clean(); -} \ No newline at end of file +} diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 9fc59816f..9350085c4 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -90,6 +90,8 @@ impl RendezvousMediator { })); } join_all(futs).await; + } else { + server.write().unwrap().close_connections(); } sleep(1.).await; } diff --git a/src/server.rs b/src/server.rs index 58aab8fd1..08b8c5b5b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,4 +1,5 @@ use crate::ipc::Data; +use bytes::Bytes; pub use connection::*; use hbb_common::{ allow_err, @@ -20,7 +21,6 @@ use std::{ sync::{Arc, Mutex, RwLock, Weak}, time::Duration, }; -use bytes::Bytes; pub mod audio_service; cfg_if::cfg_if! { @@ -140,7 +140,8 @@ pub async fn create_tcp_connection( .write_to_bytes() .unwrap_or_default(), &sk, - ).into(), + ) + .into(), ..Default::default() }); timeout(CONNECT_TIMEOUT, stream.send(&msg_out)).await??; @@ -263,6 +264,17 @@ impl Server { self.connections.remove(&conn.id()); } + pub fn close_connections(&mut self) { + let conn_inners: Vec<_> = self.connections.values_mut().collect(); + for c in conn_inners { + let mut misc = Misc::new(); + misc.set_stop_service(true); + let mut msg = Message::new(); + msg.set_misc(misc); + c.send(Arc::new(msg)); + } + } + fn add_service(&mut self, service: Box) { let name = service.name(); self.services.insert(name, service); @@ -310,9 +322,9 @@ pub fn check_zombie() { } /// Start the host server that allows the remote peer to control the current machine. -/// +/// /// # Arguments -/// +/// /// * `is_server` - Whether the current client is definitely the server. /// If true, the server will be started. /// Otherwise, client will check if there's already a server and start one if not. @@ -323,9 +335,9 @@ pub async fn start_server(is_server: bool) { } /// Start the host server that allows the remote peer to control the current machine. -/// +/// /// # Arguments -/// +/// /// * `is_server` - Whether the current client is definitely the server. /// If true, the server will be started. /// Otherwise, client will check if there's already a server and start one if not. diff --git a/src/server/connection.rs b/src/server/connection.rs index c4dc615be..96d202199 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -250,14 +250,7 @@ impl Connection { } } ipc::Data::Close => { - conn.close_manually = true; - let mut misc = Misc::new(); - misc.set_close_reason("Closed manually by the peer".into()); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - conn.send(msg_out).await; - conn.on_close("Close requested from connection manager", false).await; - SESSIONS.lock().unwrap().remove(&conn.lr.my_id); + conn.on_close_manually("connection manager").await; break; } ipc::Data::ChatMessage{text} => { @@ -404,6 +397,18 @@ impl Connection { _ => {} } } + match &msg.union { + Some(message::Union::Misc(m)) => { + match &m.union { + Some(misc::Union::StopService(_)) => { + conn.on_close_manually("stop service").await; + break; + } + _ => {}, + } + } + _ => {} + } if let Err(err) = conn.stream.send(msg).await { conn.on_close(&err.to_string(), false).await; break; @@ -1490,6 +1495,18 @@ impl Connection { self.port_forward_socket.take(); } + async fn on_close_manually(&mut self, close_from: &str) { + self.close_manually = true; + let mut misc = Misc::new(); + misc.set_close_reason("Closed manually by the peer".into()); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(msg_out).await; + self.on_close(&format!("Close requested from {}", close_from), false) + .await; + SESSIONS.lock().unwrap().remove(&self.lr.my_id); + } + fn read_dir(&mut self, dir: &str, include_hidden: bool) { let dir = dir.to_string(); self.send_fs(ipc::FS::ReadDir { diff --git a/src/server/input_service.rs b/src/server/input_service.rs index ac46726ac..08d04782b 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1,4 +1,5 @@ use super::*; +#[cfg(target_os = "linux")] use crate::common::IS_X11; #[cfg(target_os = "macos")] use dispatch::Queue; @@ -7,6 +8,7 @@ use hbb_common::{config::COMPRESS_LEVEL, get_time, protobuf::EnumOrUnknown}; use rdev::{simulate, EventType, Key as RdevKey}; use std::{ convert::TryFrom, + ops::Sub, sync::atomic::{AtomicBool, Ordering}, time::Instant, }; @@ -37,10 +39,12 @@ impl super::service::Reset for StatePos { } } -#[derive(Default)] +#[derive(Default, Clone, Copy)] struct Input { conn: i32, time: i64, + x: i32, + y: i32, } const KEY_CHAR_START: u64 = 9999; @@ -100,8 +104,16 @@ pub fn new_pos() -> GenericService { sp } +fn update_last_cursor_pos(x: i32, y: i32) { + let mut lock = LATEST_CURSOR_POS.lock().unwrap(); + if lock.1 .0 != x || lock.1 .1 != y { + (lock.0, lock.1) = (Instant::now(), (x, y)) + } +} + fn run_pos(sp: GenericService, state: &mut StatePos) -> ResultType<()> { if let Some((x, y)) = crate::get_cursor_pos() { + update_last_cursor_pos(x, y); if state.cursor_pos.0 != x || state.cursor_pos.1 != y { state.cursor_pos = (x, y); let mut msg_out = Message::new(); @@ -112,7 +124,7 @@ fn run_pos(sp: GenericService, state: &mut StatePos) -> ResultType<()> { }); let exclude = { let now = get_time(); - let lock = LATEST_INPUT.lock().unwrap(); + let lock = LATEST_INPUT_CURSOR.lock().unwrap(); if now - lock.time < 300 { lock.conn } else { @@ -170,10 +182,14 @@ lazy_static::lazy_static! { Arc::new(Mutex::new(Enigo::new())) }; static ref KEYS_DOWN: Arc>> = Default::default(); - static ref LATEST_INPUT: Arc> = Default::default(); + static ref LATEST_INPUT_CURSOR: Arc> = Default::default(); + static ref LATEST_CURSOR_POS: Arc> = Arc::new(Mutex::new((Instant::now().sub(MOUSE_MOVE_PROTECTION_TIMEOUT), (0, 0)))); } static EXITING: AtomicBool = AtomicBool::new(false); +const MOUSE_MOVE_PROTECTION_TIMEOUT: Duration = Duration::from_millis(1_000); +const MOUSE_ACTIVE_DISTANCE: i32 = 5; + // mac key input must be run in main thread, otherwise crash on >= osx 10.15 #[cfg(target_os = "macos")] lazy_static::lazy_static! { @@ -357,17 +373,54 @@ fn fix_modifiers(modifiers: &[EnumOrUnknown], en: &mut Enigo, ck: i3 } } +fn is_mouse_active_by_conn(conn: i32) -> bool { + // out of time protection + if LATEST_CURSOR_POS.lock().unwrap().0.elapsed() > MOUSE_MOVE_PROTECTION_TIMEOUT { + return true; + } + + let mut last_input = LATEST_INPUT_CURSOR.lock().unwrap(); + // last conn input may be protected + if last_input.conn != conn { + return false; + } + + // check if input is in valid range + match crate::get_cursor_pos() { + Some((x, y)) => { + let is_same_input = (last_input.x - x).abs() < MOUSE_ACTIVE_DISTANCE + && (last_input.y - y).abs() < MOUSE_ACTIVE_DISTANCE; + if !is_same_input { + last_input.x = -MOUSE_ACTIVE_DISTANCE * 2; + last_input.y = -MOUSE_ACTIVE_DISTANCE * 2; + } + is_same_input + } + None => true, + } +} + fn handle_mouse_(evt: &MouseEvent, conn: i32) { if EXITING.load(Ordering::SeqCst) { return; } + + if !is_mouse_active_by_conn(conn) { + return; + } + #[cfg(windows)] crate::platform::windows::try_change_desktop(); let buttons = evt.mask >> 3; let evt_type = evt.mask & 0x7; if evt_type == 0 { let time = get_time(); - *LATEST_INPUT.lock().unwrap() = Input { time, conn }; + *LATEST_INPUT_CURSOR.lock().unwrap() = Input { + time, + conn, + x: evt.x, + y: evt.y, + }; } let mut en = ENIGO.lock().unwrap(); #[cfg(not(target_os = "macos"))] @@ -674,19 +727,19 @@ fn sync_status(evt: &KeyEvent) -> (bool, bool) { 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), + 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); } diff --git a/src/tray.rs b/src/tray.rs index 8e9092fbf..80647fa17 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,10 +1,13 @@ -use hbb_common::log::{debug, error, info}; +use hbb_common::log::debug; +#[cfg(target_os = "linux")] +use hbb_common::log::{error, info}; #[cfg(target_os = "linux")] use libappindicator::AppIndicator; +#[cfg(target_os = "linux")] use std::env::temp_dir; use std::{ collections::HashMap, - sync::{Arc, Mutex, RwLock}, + sync::{Arc, Mutex}, }; #[cfg(target_os = "windows")] use trayicon::{MenuBuilder, TrayIconBuilder}; diff --git a/src/ui_interface.rs b/src/ui_interface.rs index cb2c178d6..29b09addc 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -20,8 +20,9 @@ use hbb_common::{ tokio::{self, sync::mpsc, time}, }; -use crate::ipc; -use crate::{common::SOFTWARE_UPDATE_URL, platform}; +use crate::{common::SOFTWARE_UPDATE_URL, ipc, platform}; +#[cfg(feature = "flutter")] +use crate::hbbs_http::account; type Message = RendezvousMessage; @@ -808,6 +809,8 @@ pub fn is_root() -> bool { #[inline] pub fn check_super_user_permission() -> bool { + #[cfg(feature = "flatpak")] + return true; #[cfg(any(windows, target_os = "linux"))] return crate::platform::check_super_user_permission().unwrap_or(false); #[cfg(not(any(windows, target_os = "linux")))] @@ -843,6 +846,21 @@ pub(crate) fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender String { + serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default() +} + // notice: avoiding create ipc connecton repeatly, // because windows named pipe has serious memory leak issue. #[tokio::main(flavor = "current_thread")] diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2f5543ead..bf03ed2d3 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -22,7 +22,6 @@ use std::collections::{HashMap, HashSet}; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; -use std::time::Duration; /// IS_IN KEYBOARD_HOOKED sciter only pub static IS_IN: AtomicBool = AtomicBool::new(false); @@ -1323,7 +1322,7 @@ impl Session { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { let func = move |event: Event| match event.event_type { - EventType::KeyPress(key) | EventType::KeyRelease(key) => { + EventType::KeyPress(..) | EventType::KeyRelease(..) => { // grab all keys if !IS_IN.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst)