diff --git a/Cargo.lock b/Cargo.lock index 39d5522ed..aedafe453 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6785,18 +6785,16 @@ dependencies = [ [[package]] name = "webm" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecb047148a12ef1fd8ab26302bca7e82036f005c3073b48e17cc1b44ec577136" +version = "1.1.0" +source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" dependencies = [ "webm-sys", ] [[package]] name = "webm-sys" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ded6ec82ccf51fe265b0b2b1579cac839574ed910c17baac58e807f8a9de7f3" +version = "1.0.4" +source = "git+https://github.com/21pages/rust-webm#d2c4d3ac133c7b0e4c0f656da710b48391981e64" dependencies = [ "cc", ] diff --git a/Dockerfile b/Dockerfile index 8e44adb74..3aa093e01 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,54 @@ -FROM debian +FROM debian:bullseye-slim WORKDIR / -RUN apt update -y && apt install -y g++ gcc git curl nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev cmake ninja-build && rm -rf /var/lib/apt/lists/* +ARG DEBIAN_FRONTEND=noninteractive +RUN apt update -y && \ + apt install --yes --no-install-recommends \ + g++ \ + gcc \ + git \ + curl \ + nasm \ + yasm \ + libgtk-3-dev \ + clang \ + libxcb-randr0-dev \ + libxdo-dev \ + libxfixes-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libasound2-dev \ + libpulse-dev \ + make \ + cmake \ + unzip \ + zip \ + sudo \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + ca-certificates \ + ninja-build && \ + rm -rf /var/lib/apt/lists/* -RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg -RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics -RUN /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom +RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg && \ + /vcpkg/bootstrap-vcpkg.sh -disableMetrics && \ + /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom + +RUN groupadd -r user && \ + useradd -r -g user user --home /home/user && \ + mkdir -p /home/user/rustdesk && \ + chown -R user: /home/user && \ + echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user -RUN groupadd -r user && useradd -r -g user user --home /home/user && mkdir -p /home/user && chown user /home/user && echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user WORKDIR /home/user RUN curl -LO https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so + USER user -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh -RUN chmod +x rustup.sh -RUN ./rustup.sh -y +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh && \ + chmod +x rustup.sh && \ + ./rustup.sh -y USER root ENV HOME=/home/user -COPY ./entrypoint / -ENTRYPOINT ["/entrypoint"] +COPY ./entrypoint.sh / +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md index 019442872..924cd1786 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info. ## Dependencies -Desktop versions use [Sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only. +Desktop versions use Flutter or Sciter (deprecated) for GUI, this tutorial is for Sciter only, since it is easier and more friendly to starter. Check out our [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for building Flutter version. Please download Sciter dynamic library yourself. @@ -135,34 +135,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Change Wayland to X11 (Xorg) - -RustDesk does not support Wayland. Check [this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) to configuring Xorg as the default GNOME session. - -## Wayland support - -Wayland does not seem to provide any API for sending keypresses to other windows. Therefore, the RustDesk uses an API from a lower level, namely the `/dev/uinput` device (Linux kernel level). - -When Wayland is the controlled side, you have to start in the following way: -```bash -# Start uinput service -$ sudo rustdesk --service -$ rustdesk -``` -**Notice**: Wayland screen recording uses different interfaces. RustDesk currently only supports org.freedesktop.portal.ScreenCast. -```bash -$ dbus-send --session --print-reply \ - --dest=org.freedesktop.portal.Desktop \ - /org/freedesktop/portal/desktop \ - org.freedesktop.DBus.Properties.Get \ - string:org.freedesktop.portal.ScreenCast string:version -# Not support -Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” -# Support -method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 - variant uint32 4 -``` - ## How to build with Docker Begin by cloning the repository and building the Docker container: @@ -198,12 +170,12 @@ Please ensure that you are running these commands from the root of the RustDesk - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control -- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: obsolete Sciter UI (deprecated) - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code -- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for desktop and mobile - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client ## Snapshots diff --git a/docs/README-AR.md b/docs/README-AR.md index 80948fb39..9b72eb18d 100644 --- a/docs/README-AR.md +++ b/docs/README-AR.md @@ -118,10 +118,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### X11 (Xorg) إلى Wayland تغيير - -افتراضية GNOME session ك Xorg إتبع [هذه](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) الخطوات لإعداد Wayland لا تدعم RustDesk - ## Docker طريقة البناء باستخدام ابدأ باستنساخ المستودع وبناء الكونتاينر: diff --git a/docs/README-CS.md b/docs/README-CS.md index 15e576c47..e7cf998f3 100644 --- a/docs/README-CS.md +++ b/docs/README-CS.md @@ -111,10 +111,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Změna z Wayland na X11 (Xorg) - -RustDesk (zatím) nepodporuje zobrazovací server Wayland. Jak nastavit Xorg jako výchozí pro relace v prostředí GNOME naleznete [zde](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/). - ## Jak sestavit prostřednictvím Docker kontejnerizace Začněte tím, že si naklonujete tento repozitář a sestavíte docker kontejner: diff --git a/docs/README-DA.md b/docs/README-DA.md index af2f5937f..e0bc11829 100644 --- a/docs/README-DA.md +++ b/docs/README-DA.md @@ -108,33 +108,6 @@ mv libsciter-gtk.so target/debug cargo run ``` -### Skift Wayland til X11 (Xorg) - -RustDesk understøtter ikke Wayland. Tjek [dette](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) for at konfigurere Xorg som standard GNOME-session. - -## Wayland-support - -Wayland ser ikke ud til at levere nogen API til at sende tastetryk til andre vinduer. Derfor bruger rustdesk et API fra et lavere niveau, nemlig `/dev/uinput`-enheden (Linux-kerneniveau). - -Når wayland er den kontrollerede side, skal du starte på følgende måde: -```bash -# Start uinput service -$ sudo rustdesk --service -$ rustdesk -``` -**Bemærk**: Wayland-skærmoptagelse bruger forskellige grænseflader. RustDesk understøtter i øjeblikket kun org.freedesktop.portal.ScreenCast. -```bash -$ dbus-send --session --print-reply \ - --dest=org.freedesktop.portal.Desktop \ - /org/freedesktop/portal/desktop \ - org.freedesktop.DBus.Properties.Get \ - string:org.freedesktop.portal.ScreenCast string:version -# Not support -Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” -# Support -method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 - variant uint32 4 -``` ## Sådan bygger du med Docker ```sh diff --git a/docs/README-DE.md b/docs/README-DE.md index 5ac370a87..066c13f99 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -133,34 +133,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Wayland zu X11 (Xorg) ändern - -RustDesk unterstützt Wayland nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard-GNOME-Sitzung zu nutzen. - -## Wayland-Unterstützung - -Wayland scheint keine API für das Senden von Tastatureingaben an andere Fenster zu bieten. Daher verwendet RustDesk eine API von einer niedrigeren Ebene, nämlich dem Gerät `/dev/uinput` (Linux-Kernelebene). - -Wenn Wayland die kontrollierte Seite ist, müssen Sie wie folgt vorgehen: -```bash -# Dienst uinput starten -$ sudo rustdesk --service -$ rustdesk -``` -**Hinweis**: Die Wayland-Bildschirmaufnahme verwendet verschiedene Schnittstellen. RustDesk unterstützt derzeit nur org.freedesktop.portal.ScreenCast. -```bash -$ dbus-send --session --print-reply \ - --dest=org.freedesktop.portal.Desktop \ - /org/freedesktop/portal/desktop \ - org.freedesktop.DBus.Properties.Get \ - string:org.freedesktop.portal.ScreenCast string:version -# Keine Unterstützung -Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” -# Unterstützung -method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 - variant uint32 4 -``` - ## Auf Docker kompilieren Beginnen Sie damit, das Repository zu klonen und den Docker-Container zu bauen: diff --git a/docs/README-EO.md b/docs/README-EO.md index be1538089..2cfc9ee6a 100644 --- a/docs/README-EO.md +++ b/docs/README-EO.md @@ -104,10 +104,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Ŝanĝi Wayland por X11 (Xorg) - -RustDesk ne subtenas Wayland. Kontrolu [tion](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) por agordi Xorg kiel defaŭlta sesio GNOME. - ## Kiel kompili kun Docker Komencu klonante la deponejon kaj kompilu la konteneron Docker: diff --git a/docs/README-ES.md b/docs/README-ES.md index 19a7335d3..02424ec00 100644 --- a/docs/README-ES.md +++ b/docs/README-ES.md @@ -113,34 +113,6 @@ mv libsciter-gtk.so target/debug cargo run ``` -### Cambia Wayland a X11 (Xorg) - -RustDesk no soporta Wayland. Lee [esto](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar Xorg en la sesión por defecto de GNOME. - -## Soporte para Wayland - -Wayland no parece proporcionar ninguna API para enviar pulsaciones de teclas a otras ventanas. Por lo tanto, rustdesk usa una API de nivel bajo, a saber, el dispositivo `/dev/uinput` (a nivel del kernel de Linux). - -Cuando wayland esta del lado controlado, hay que iniciar de la siguiente manera: -```bash -# Empezar el servicio uinput -$ sudo rustdesk --service -$ rustdesk -``` -**Aviso**: La grabación de pantalla de Wayland utiliza diferentes interfaces. RustDesk actualmente sólo soporta org.freedesktop.portal.ScreenCast -```bash -$ dbus-send --session --print-reply \ - --dest=org.freedesktop.portal.Desktop \ - /org/freedesktop/portal/desktop \ - org.freedesktop.DBus.Properties.Get \ - string:org.freedesktop.portal.ScreenCast string:version -# No soportado -Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” -# Soportado -method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 - variant uint32 4 -``` - ## Como compilar con Docker Empieza clonando el repositorio y compilando el contenedor de docker: diff --git a/docs/README-FA.md b/docs/README-FA.md index 989b0047c..d07dadd09 100644 --- a/docs/README-FA.md +++ b/docs/README-FA.md @@ -112,10 +112,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### تغییر Wayland به (X11 (Xorg - -راست‌دسک از Wayland پشتیبانی نمی کند. برای جایگزنی Xorg به عنوان پیش‌فرض GNOM، [اینجا](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) را کلیک کنید. - ## نحوه ساخت با داکر این مخزن Git را دریافت کنید و کانتینر را به روش زیر بسازید diff --git a/docs/README-FI.md b/docs/README-FI.md index 195d29c84..988729a0f 100644 --- a/docs/README-FI.md +++ b/docs/README-FI.md @@ -104,10 +104,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Vaihda Wayland-ympäristö X11 (Xorg)-ympäristöön - -RustDesk ei tue Waylandia. Tarkista [tämä](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) asettamalla Xorg oletus GNOME-istuntoon. - ## Kuinka rakennetaan Dockerin kanssa Aloita kloonaamalla tietovarasto ja rakentamalla docker-säiliö: diff --git a/docs/README-FR.md b/docs/README-FR.md index 39f09a625..ea55ec468 100644 --- a/docs/README-FR.md +++ b/docs/README-FR.md @@ -104,10 +104,6 @@ mv libsciter-gtk.so target/debug Exécution du cargo ``` -### Changer Wayland en X11 (Xorg) - -RustDesk ne supporte pas Wayland. Lisez [cela](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) pour configurer Xorg comme la session GNOME par défaut. - ## Comment construire avec Docker Commencez par cloner le dépôt et construire le conteneur Docker : diff --git a/docs/README-GR.md b/docs/README-GR.md index c720dd823..f324cfa95 100644 --- a/docs/README-GR.md +++ b/docs/README-GR.md @@ -133,34 +133,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Αλλαγή του Wayland σε X11 (Xorg) - -Το RustDesk δεν υποστηρίζει το πρωτόκολλο Wayland. Διαβάστε [εδώ](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) ώστε να ορίσετε το Xorg ως το προκαθορισμένο GNOME περιβάλλον. - -## Υποστήριξη Wayland - -Το Wayland προς το παρόν δεν διαθέτει κάποιο API το οποίο να στέλνει τα πατήματα πλήκτρων στα υπόλοιπα παράθυρα. Για τον λόγο αυτό, το Rustdesk χρησιμοποιεί ένα API από κατώτερο επίπεδο, όπως το `/dev/uinput` (Linux kernel level). - -Σε περίπτωση που το Wayland είναι η ελεγχόμενη πλευρά, θα πρέπει να ξεκινήσετε με τον παρακάτω τρόπο: -```bash -# Start uinput service -$ sudo rustdesk --service -$ rustdesk -``` -**Σημείωση**: Η εγγραφή οθόνης του Wayland χρησιμοποιεί διαφορετικές διεπαφές. Το RustDesk προς το παρόν υποστηρίζει μόνο org.freedesktop.portal.ScreenCast. -```bash -$ dbus-send --session --print-reply \ - --dest=org.freedesktop.portal.Desktop \ - /org/freedesktop/portal/desktop \ - org.freedesktop.DBus.Properties.Get \ - string:org.freedesktop.portal.ScreenCast string:version -# Not support -Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” -# Support -method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 - variant uint32 4 -``` - ## Πως να κάνετε build στο Docker Ξεκινήστε κλωνοποιώντας το αποθετήριο και κάνοντας build το docker container: diff --git a/docs/README-HU.md b/docs/README-HU.md index 8965a9b1e..d47a775d0 100644 --- a/docs/README-HU.md +++ b/docs/README-HU.md @@ -116,10 +116,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Válts Wayland-ról X11-re (Xorg) - -A RustDesk nem támogatja a Waylendet. [Itt](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) található egy tutorial amelynek segítségével beállíthatod a Xorg-ot mint alap GNOME session. - ## Hogyan építs Dockerrel Kezdjünk a repo clónozásával, majd pedig a Docker container megépítésével: diff --git a/docs/README-ID.md b/docs/README-ID.md index 9999eb72f..be8003798 100644 --- a/docs/README-ID.md +++ b/docs/README-ID.md @@ -128,37 +128,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Mengubah Wayland ke X11 (Xorg) - -RustDesk tidak mendukung Wayland. Cek [ini](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) untuk mengonfigurasi Xorg sebagai sesi standar di GNOME. - -## Kompatibilitas dengan Wayland - -Sepertinya Wayland tidak memiliki API untuk mengirimkan ketukan tombol ke jendela lain. Maka dari itu, RustDesk menggunakan API dari level yang lebih rendah, lebih tepatnya perangkat `/dev/uinput` (linux kernel level) - -Saat Wayland menjadi sisi yang dikendalikan atau sisi yang sedang diremote, kamu harus memulai dengan cara ini - -```bash -# Start uinput service -$ sudo rustdesk --service -$ rustdesk -``` - -**Harap Diperhatikan**: Saat Perekaman layar menggunakan Wayland antarmuka (UI) yang ditampilkan akan berbeda. Untuk saat ini RustDesk hanya mendukung org.freedesktop.portal.ScreenCast. - -```bash -$ dbus-send --session --print-reply \ - --dest=org.freedesktop.portal.Desktop \ - /org/freedesktop/portal/desktop \ - org.freedesktop.DBus.Properties.Get \ - string:org.freedesktop.portal.ScreenCast string:version -# Not support -Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” -# Support -method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 - variant uint32 4 -``` - ## Cara Build dengan Docker Mulailah dengan melakukan kloning (clone) repositori dan build dengan docker container: diff --git a/docs/README-IT.md b/docs/README-IT.md index c1c46bfed..6cef02804 100644 --- a/docs/README-IT.md +++ b/docs/README-IT.md @@ -109,11 +109,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Cambiare Wayland in X11 (Xorg) - -RustDesk non supporta Wayland. -Controlla [qui](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) per configurare Xorg come sessione predefinita di GNOME. - ## Come compilare con Docker Clona il repository e compila i container docker: diff --git a/docs/README-JP.md b/docs/README-JP.md index 44f811eec..d822cff17 100644 --- a/docs/README-JP.md +++ b/docs/README-JP.md @@ -114,11 +114,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Wayland の場合、X11(Xorg)に変更します - -RustDeskはWaylandをサポートしていません。 - [こちら](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) を確認して、XorgをデフォルトのGNOMEセッションとして構成します。 - ## Dockerでビルドする方法 リポジトリのクローンを作成し、Dockerコンテナを構築することから始めます。 diff --git a/docs/README-KR.md b/docs/README-KR.md index dacb092e7..829f00fe9 100644 --- a/docs/README-KR.md +++ b/docs/README-KR.md @@ -112,10 +112,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Wayland 일 경우, X11(Xorg)로 변경 - -RustDesk는 Wayland를 지원하지 않습니다. [링크](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/)를 확인해서 Xorg 기본값의 GNOME 세션을 구성합니다. - ## Docker에 빌드하는 방법 레포지토리를 클론하고, Docker 컨테이너 구성하는 것으로 시작합니다. diff --git a/docs/README-ML.md b/docs/README-ML.md index a73fd7815..6aaaf6077 100644 --- a/docs/README-ML.md +++ b/docs/README-ML.md @@ -103,10 +103,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### വേലാൻഡ് X11 (Xorg) ആയി മാറ്റുക - -RustDesk Wayland-നെ പിന്തുണയ്ക്കുന്നില്ല. സ്ഥിരസ്ഥിതി ഗ്നോം സെഷനായി Xorg കോൺഫിഗർ ചെയ്യുന്നതിന് [ഇത്](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) പരിശോധിക്കുക. - ## ഡോക്കർ ഉപയോഗിച്ച് എങ്ങനെ നിർമ്മിക്കാം റെപ്പോസിറ്റോറി ക്ലോണുചെയ്‌ത് ഡോക്കർ കണ്ടെയ്‌നർ നിർമ്മിക്കുന്നതിലൂടെ ആരംഭിക്കുക: diff --git a/docs/README-NL.md b/docs/README-NL.md index bec83a285..ee5e98a85 100644 --- a/docs/README-NL.md +++ b/docs/README-NL.md @@ -130,34 +130,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Wissel van Wayland naar X11 (Xorg) - -RustDesk ondersteunt Wayland niet. Lees [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) hoe je Xorg als standaardsessie kunt instellen voor GNOME. - -## Wayland support - -Wayland lijkt geen API te bieden voor het verzenden van toetsaanslagen naar andere vensters. Daarom gebruikt de rustdesk een API van een lager niveau, namelijk het `/dev/uinput` apparaat (Linux kernel niveau). - -Als wayland de gecontroleerde kant is, moet je op de volgende manier beginnen: -```bash -# Start uinput service -$ sudo rustdesk --service -$ rustdesk -``` -**Let op**: Wayland schermopname gebruikt verschillende interfaces. RustDesk ondersteunt momenteel alleen org.freedesktop.portal.ScreenCast. -```bash -$ dbus-send --session --print-reply \ - --dest=org.freedesktop.portal.Desktop \ - /org/freedesktop/portal/desktop \ - org.freedesktop.DBus.Properties.Get \ - string:org.freedesktop.portal.ScreenCast string:version -# Not support -Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” -# Support -method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 - variant uint32 4 -``` - ## Bouwen met Docker Begin met het klonen van de repository en het bouwen van de docker container: diff --git a/docs/README-PL.md b/docs/README-PL.md index ba27af04d..3809c58bd 100644 --- a/docs/README-PL.md +++ b/docs/README-PL.md @@ -128,34 +128,6 @@ mv libsciter-gtk.so target/debug cargo run ``` -### Zmień Wayland na X11 (Xorg) - -RustDesk nie obsługuje Waylanda. Sprawdź [tutaj](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), jak skonfigurować Xorg jako domyślną sesję GNOME. - -## Wspracie Wayland - -Wygląda na to, że Wayland nie wspiera żadnego API do wysyłania naciśnięć klawiszy do innych okien. Dlatego rustdesk używa API z niższego poziomu, urządzenia o nazwie `/dev/uinput` (poziom jądra Linux). - -Gdy po stronie kontrolowanej pracuje Wayland, musisz uruchomić program w następujący sposób: -```bash -# Start uinput service -$ sudo rustdesk --service -$ rustdesk -``` -**Uwaga**: Nagrywanie ekranu Wayland wykorzystuje różne interfejsy. RustDesk obecnie obsługuje tylko org.freedesktop.portal.ScreenCast. -```bash -$ dbus-send --session --print-reply \ - --dest=org.freedesktop.portal.Desktop \ - /org/freedesktop/portal/desktop \ - org.freedesktop.DBus.Properties.Get \ - string:org.freedesktop.portal.ScreenCast string:version -# Not support -Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” -# Support -method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 - variant uint32 4 -``` - ## Jak kompilować za pomocą Dockera Rozpocznij od sklonowania repozytorium i stworzenia kontenera docker: diff --git a/docs/README-PTBR.md b/docs/README-PTBR.md index 6e6f01fce..3ae092db8 100644 --- a/docs/README-PTBR.md +++ b/docs/README-PTBR.md @@ -104,10 +104,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Mude Wayland para X11 (Xorg) - -RustDesk não suporta Wayland. Veja [esse link](https://docs.fedoraproject.org/pt_BR/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar o Xorg como a sessão padrão do GNOME. - ## Como compilar com Docker Comece clonando o repositório e montando o container docker: diff --git a/docs/README-RU.md b/docs/README-RU.md index 01710f084..959c33b10 100644 --- a/docs/README-RU.md +++ b/docs/README-RU.md @@ -114,10 +114,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Смените Wayland на X11 (Xorg) - -RustDesk не поддерживает Wayland. Смотрите [этот документ](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) для настройки Xorg в качестве сеанса GNOME по умолчанию. - ## Как собрать с помощью Docker Начните с клонирования репозитория и создания docker-контейнера: diff --git a/docs/README-TR.md b/docs/README-TR.md index 590ead0df..3afae98b0 100644 --- a/docs/README-TR.md +++ b/docs/README-TR.md @@ -138,34 +138,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Wayland'ı X11 (Xorg) Olarak Değiştirme - -RustDesk, Wayland'ı desteklemez. Xorg'u GNOME oturumu olarak varsayılan olarak ayarlamak için [burayı](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) kontrol edin. - -## Wayland Desteği - -Wayland'ın diğer pencerelere tuş vuruşu göndermek için herhangi bir API sağlamadığı görünmektedir. Bu nedenle, RustDesk daha düşük bir seviyeden, yani Linux çekirdek seviyesindeki `/dev/uinput` cihazının API'sini kullanır. - -Wayland tarafı kontrol edildiğinde, aşağıdaki şekilde başlatmanız gerekir: -```bash -# uinput servisini başlatın -$ sudo rustdesk --service -$ rustdesk -``` -**Uyarı**: Wayland ekran kaydı farklı arayüzler kullanır. RustDesk şu anda yalnızca org.freedesktop.portal.ScreenCast'ı destekler. -```bash -$ dbus-send --session --print-reply \ - --dest=org.freedesktop.portal.Desktop \ - /org/freedesktop/portal/desktop \ - org.freedesktop.DBus.Properties.Get \ - string:org.freedesktop.portal.ScreenCast string:version -# Desteklenmez -Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” -# Desteklenir -method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 - variant uint32 4 -``` - ## Docker ile Derleme Nasıl Yapılır Öncelikle deposunu klonlayın ve Docker konteynerini oluşturun: diff --git a/docs/README-UA.md b/docs/README-UA.md index 01914cfc2..275033977 100644 --- a/docs/README-UA.md +++ b/docs/README-UA.md @@ -131,10 +131,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Змініть Wayland на X11 (Xorg) - -RustDesk не підтримує Wayland. Дивіться [цей документ](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) для налаштування Xorg як сеансу GNOME за замовчуванням. - ## Як зібрати за допомогою Docker Почніть з клонування сховища та створення docker-контейнера: diff --git a/docs/README-VN.md b/docs/README-VN.md index ea2c62ead..de6d6b800 100644 --- a/docs/README-VN.md +++ b/docs/README-VN.md @@ -116,10 +116,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Chuyển từ Wayland sang X11 (Xorg) - -RustDesk hiện không hỗ trợ Wayland. Hãy xem [đường linh ở đây](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) cách để cài đặt Xorg làm session mặc định của GNOME. - ## Cách để build sử dụng Docker Bắt đầu bằng cách sao chép repo này về máy tính và build cái Docker cointainer: diff --git a/docs/README-ZH.md b/docs/README-ZH.md index 7967f7d30..1229f8ad5 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -134,39 +134,6 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### 把 Wayland 修改成 X11 (Xorg) - -RustDesk 暂时不支持 Wayland,不过正在积极开发中。 -> [点我](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) -查看如何将 Xorg 设置成默认的 GNOME session. - -## Wayland 支持 - -Wayland 似乎没有提供任何将按键发送到其他窗口的 API. 因此, RustDesk 使用较低级别的 API, 即 `/dev/uinput` devices (Linux kernal level). - -当 Wayland 是受控方时,您必须以下列方式开始操作: - -```bash -# Start uinput service -$ sudo rustdesk --service -$ rustdesk -``` - -**Notice**: Wayland 屏幕录制使用不同的接口. RustDesk 目前只支持 org.freedesktop.portal.ScreenCast. - -```bash -$ dbus-send --session --print-reply \ - --dest=org.freedesktop.portal.Desktop \ - /org/freedesktop/portal/desktop \ - org.freedesktop.DBus.Properties.Get \ - string:org.freedesktop.portal.ScreenCast string:version -# Not support -Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” -# Support -method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 - variant uint32 4 -``` - ## 使用 Docker 编译 克隆版本库并构建 Docker 容器: diff --git a/entrypoint b/entrypoint.sh similarity index 100% rename from entrypoint rename to entrypoint.sh diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 1da2dc60a..9c8404c56 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -14,8 +14,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; -import 'package:flutter_hbb/models/desktop_render_texture.dart'; import 'package:flutter_hbb/main.dart'; +import 'package:flutter_hbb/models/desktop_render_texture.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; @@ -53,6 +53,9 @@ int androidVersion = 0; int windowsBuildNumber = 0; DesktopType? desktopType; +bool get isMainDesktopWindow => + desktopType == DesktopType.main || desktopType == DesktopType.cm; + /// Check if the app is running with single view mode. bool isSingleViewApp() { return desktopType == DesktopType.cm; @@ -955,7 +958,7 @@ class CustomAlertDialog extends StatelessWidget { void msgBox(SessionID sessionId, String type, String title, String text, String link, OverlayDialogManager dialogManager, - {bool? hasCancel, ReconnectHandle? reconnect}) { + {bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) { dialogManager.dismissAll(); List buttons = []; bool hasOk = false; @@ -995,22 +998,21 @@ void msgBox(SessionID sessionId, String type, String title, String text, dialogManager.dismissAll(); })); } - if (reconnect != null && title == "Connection Error") { + if (reconnect != null && + title == "Connection Error" && + reconnectTimeout != null) { // `enabled` is used to disable the dialog button once the button is clicked. final enabled = true.obs; - final button = Obx( - () => dialogButton( - 'Reconnect', - isOutline: true, - onPressed: enabled.isTrue - ? () { - // Disable the button - enabled.value = false; - reconnect(dialogManager, sessionId, false); - } - : null, - ), - ); + final button = Obx(() => _ReconnectCountDownButton( + second: reconnectTimeout, + onPressed: enabled.isTrue + ? () { + // Disable the button + enabled.value = false; + reconnect(dialogManager, sessionId, false); + } + : null, + )); buttons.insert(0, button); } if (link.isNotEmpty) { @@ -1491,8 +1493,8 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async { late Offset position; late Size sz; late bool isMaximized; - bool isFullscreen = stateGlobal.fullscreen || - (Platform.isMacOS && stateGlobal.closeOnFullscreen); + bool isFullscreen = stateGlobal.fullscreen.isTrue || + (Platform.isMacOS && stateGlobal.closeOnFullscreen == true); setFrameIfMaximized() { if (isMaximized) { final pos = bind.getLocalFlutterOption(k: kWindowPrefix + type.name); @@ -1670,8 +1672,10 @@ Future _adjustRestoreMainWindowOffset( /// Restore window position and size on start /// Note that windowId must be provided if it's subwindow +// +// display is used to set the offset of the window in individual display mode. Future restoreWindowPosition(WindowType type, - {int? windowId, String? peerId}) async { + {int? windowId, String? peerId, int? display}) async { if (bind .mainGetEnv(key: "DISABLE_RUSTDESK_RESTORE_WINDOW_POSITION") .isNotEmpty) { @@ -1707,14 +1711,22 @@ Future restoreWindowPosition(WindowType type, debugPrint("no window position saved, ignoring position restoration"); return false; } - if (type == WindowType.RemoteDesktop && - !isRemotePeerPos && - windowId != null) { - if (lpos.offsetWidth != null) { - lpos.offsetWidth = lpos.offsetWidth! + windowId * 20; + if (type == WindowType.RemoteDesktop) { + if (!isRemotePeerPos && windowId != null) { + if (lpos.offsetWidth != null) { + lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset; + } + if (lpos.offsetHeight != null) { + lpos.offsetHeight = lpos.offsetHeight! + windowId * kNewWindowOffset; + } } - if (lpos.offsetHeight != null) { - lpos.offsetHeight = lpos.offsetHeight! + windowId * 20; + if (display != null) { + if (lpos.offsetWidth != null) { + lpos.offsetWidth = lpos.offsetWidth! + display * kNewWindowOffset; + } + if (lpos.offsetHeight != null) { + lpos.offsetHeight = lpos.offsetHeight! + display * kNewWindowOffset; + } } } @@ -2012,6 +2024,10 @@ connect( final idController = Get.find(); idController.text = formatID(id); } + if (Get.isRegistered()){ + final fieldTextEditingController = Get.find(); + fieldTextEditingController.text = formatID(id); + } } catch (_) {} } id = id.replaceAll(' ', ''); @@ -2605,3 +2621,183 @@ bool isChooseDisplayToOpenInNewWindow(PeerInfo pi, SessionID sessionId) => pi.isSupportMultiDisplay && useTextureRender && bind.sessionGetDisplaysAsIndividualWindows(sessionId: sessionId) == 'Y'; + +Future> getScreenListWayland() async { + final screenRectList = []; + if (isMainDesktopWindow) { + for (var screen in await window_size.getScreenList()) { + final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor; + double l = screen.frame.left; + double t = screen.frame.top; + double r = screen.frame.right; + double b = screen.frame.bottom; + final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale); + screenRectList.add(rect); + } + } else { + final screenList = await rustDeskWinManager.call( + WindowType.Main, kWindowGetScreenList, ''); + try { + for (var screen in jsonDecode(screenList.result) as List) { + final scale = kIgnoreDpi ? 1.0 : screen['scaleFactor']; + double l = screen['frame']['l']; + double t = screen['frame']['t']; + double r = screen['frame']['r']; + double b = screen['frame']['b']; + final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale); + screenRectList.add(rect); + } + } catch (e) { + debugPrint('Failed to parse screenList: $e'); + } + } + return screenRectList; +} + +Future> getScreenListNotWayland() async { + final screenRectList = []; + final displays = bind.mainGetDisplays(); + if (displays.isEmpty) { + return screenRectList; + } + try { + for (var display in jsonDecode(displays) as List) { + // to-do: scale factor ? + // final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor; + double l = display['x'].toDouble(); + double t = display['y'].toDouble(); + double r = (display['x'] + display['w']).toDouble(); + double b = (display['y'] + display['h']).toDouble(); + screenRectList.add(Rect.fromLTRB(l, t, r, b)); + } + } catch (e) { + debugPrint('Failed to parse displays: $e'); + } + return screenRectList; +} + +Future> getScreenRectList() async { + return bind.mainCurrentIsWayland() + ? await getScreenListWayland() + : await getScreenListNotWayland(); +} + +openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi) { + final displays = i == kAllDisplayValue + ? List.generate(pi.displays.length, (index) => index) + : [i]; + bind.sessionSwitchDisplay( + sessionId: ffi.sessionId, value: Int32List.fromList(displays)); + ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, ffi.id); +} + +// Open new tab or window to show this monitor. +// For now just open new window. +// +// screenRect is used to move the new window to the specified screen and set fullscreen. +openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi, + {Rect? screenRect}) { + final args = { + 'window_id': stateGlobal.windowId, + 'peer_id': peerId, + 'display': i, + 'display_count': pi.displays.length, + }; + if (screenRect != null) { + args['screen_rect'] = { + 'l': screenRect.left, + 't': screenRect.top, + 'r': screenRect.right, + 'b': screenRect.bottom, + }; + } + DesktopMultiWindow.invokeMethod( + kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args)); +} + +tryMoveToScreenAndSetFullscreen(Rect? screenRect) async { + if (screenRect == null) { + return; + } + final wc = WindowController.fromWindowId(stateGlobal.windowId); + final curFrame = await wc.getFrame(); + final frame = + Rect.fromLTWH(screenRect.left + 30, screenRect.top + 30, 600, 400); + if (stateGlobal.fullscreen.isTrue && + curFrame.left <= frame.left && + curFrame.top <= frame.top && + curFrame.width >= frame.width && + curFrame.height >= frame.height) { + return; + } + await wc.setFrame(frame); + // An duration is needed to avoid the window being restored after fullscreen. + Future.delayed(Duration(milliseconds: 300), () async { + stateGlobal.setFullscreen(true); + }); +} + +parseParamScreenRect(Map params) { + Rect? screenRect; + if (params['screen_rect'] != null) { + double l = params['screen_rect']['l']; + double t = params['screen_rect']['t']; + double r = params['screen_rect']['r']; + double b = params['screen_rect']['b']; + screenRect = Rect.fromLTRB(l, t, r, b); + } + return screenRect; +} + +class _ReconnectCountDownButton extends StatefulWidget { + _ReconnectCountDownButton({ + Key? key, + required this.second, + required this.onPressed, + }) : super(key: key); + final VoidCallback? onPressed; + final int second; + + @override + State<_ReconnectCountDownButton> createState() => + _ReconnectCountDownButtonState(); +} + +class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> { + late int _countdownSeconds = widget.second; + + Timer? _timer; + + @override + void initState() { + super.initState(); + _startCountdownTimer(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + void _startCountdownTimer() { + _timer = Timer.periodic(Duration(seconds: 1), (timer) { + if (_countdownSeconds <= 0) { + timer.cancel(); + } else { + setState(() { + _countdownSeconds--; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return dialogButton( + '${translate('Reconnect')} (${_countdownSeconds}s)', + onPressed: widget.onPressed, + isOutline: true, + ); + } +} diff --git a/flutter/lib/common/widgets/autocomplete.dart b/flutter/lib/common/widgets/autocomplete.dart new file mode 100644 index 000000000..9c14eab7f --- /dev/null +++ b/flutter/lib/common/widgets/autocomplete.dart @@ -0,0 +1,196 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/formatter/id_formatter.dart'; +import '../../../models/platform_model.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/peer_card.dart'; + + + Future> getAllPeers() async { + Map recentPeers = jsonDecode(await bind.mainLoadRecentPeersSync()); + Map lanPeers = jsonDecode(await bind.mainLoadLanPeersSync()); + Map abPeers = jsonDecode(await bind.mainLoadAbSync()); + Map groupPeers = jsonDecode(await bind.mainLoadGroupSync()); + + Map combinedPeers = {}; + + void _mergePeers(Map peers) { + if (peers.containsKey("peers")) { + dynamic peerData = peers["peers"]; + + if (peerData is String) { + try { + peerData = jsonDecode(peerData); + } catch (e) { + print("Error decoding peers: $e"); + return; + } + } + + if (peerData is List) { + for (var peer in peerData) { + if (peer is Map && peer.containsKey("id")) { + String id = peer["id"]; + if (id != null && !combinedPeers.containsKey(id)) { + combinedPeers[id] = peer; + } + } + } + } + } + } + + _mergePeers(recentPeers); + _mergePeers(lanPeers); + _mergePeers(abPeers); + _mergePeers(groupPeers); + + List parsedPeers = []; + + for (var peer in combinedPeers.values) { + parsedPeers.add(Peer.fromJson(peer)); + } + return parsedPeers; + } + + class AutocompletePeerTile extends StatefulWidget { + final IDTextEditingController idController; + final Peer peer; + + const AutocompletePeerTile({ + Key? key, + required this.idController, + required this.peer, + }) : super(key: key); + + @override + _AutocompletePeerTileState createState() => _AutocompletePeerTileState(); +} + +class _AutocompletePeerTileState extends State{ + List _frontN(List list, int n) { + if (list.length <= n) { + return list; + } else { + return list.sublist(0, n); + } + } + @override + Widget build(BuildContext context){ + final double _tileRadius = 5; + final name = + '${widget.peer.username}${widget.peer.username.isNotEmpty && widget.peer.hostname.isNotEmpty ? '@' : ''}${widget.peer.hostname}'; + final greyStyle = TextStyle( + fontSize: 11, + color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); + final child = GestureDetector( + onTap: () { + setState(() { + widget.idController.id = widget.peer.id; + FocusScope.of(context).unfocus(); + }); + }, + child: + Container( + height: 42, + margin: EdgeInsets.only(bottom: 5), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + decoration: BoxDecoration( + color: str2color('${widget.peer.id}${widget.peer.platform}', 0x7f), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(_tileRadius), + bottomLeft: Radius.circular(_tileRadius), + ), + ), + alignment: Alignment.center, + width: 42, + height: null, + child: Padding( + padding: EdgeInsets.all(6), + child: getPlatformImage(widget.peer.platform, size: 30) + ) + ), + Expanded( + child: Container( + padding: EdgeInsets.only(left: 10), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.only( + topRight: Radius.circular(_tileRadius), + bottomRight: Radius.circular(_tileRadius), + ), + ), + child: Row( + children: [ + Expanded( + child: Container( + margin: EdgeInsets.only(top: 2), + child: Container( + margin: EdgeInsets.only(top: 2), + child: Column( + children: [ + Container( + margin: EdgeInsets.only(top: 2), + child: Row(children: [ + getOnline(8, widget.peer.online), + Expanded( + child: Text( + widget.peer.alias.isEmpty ? formatID(widget.peer.id) : widget.peer.alias, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleSmall, + )), + !widget.peer.alias.isEmpty? + Padding( + padding: const EdgeInsets.only(left: 5, right: 5), + child: Text( + "(${widget.peer.id})", + style: greyStyle, + overflow: TextOverflow.ellipsis, + ) + ) + : Container(), + ])), + Align( + alignment: Alignment.centerLeft, + child: Text( + name, + style: greyStyle, + textAlign: TextAlign.start, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ) + ))), + ], + ) + ), + ) + ], + ))); + final colors = + _frontN(widget.peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList(); + return Tooltip( + message: isMobile + ? '' + : widget.peer.tags.isNotEmpty + ? '${translate('Tags')}: ${widget.peer.tags.join(', ')}' + : '', + child: Stack(children: [ + child, + if (colors.isNotEmpty) + Positioned( + top: 5, + right: 10, + child: CustomPaint( + painter: TagPainter(radius: 3, colors: colors), + ), + ) + ]), + ); + } + } \ No newline at end of file diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 922f88225..a071d8f40 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -19,7 +19,7 @@ import 'dart:math' as math; typedef PopupMenuEntryBuilder = Future>> Function(BuildContext); -enum PeerUiType { grid, list } +enum PeerUiType { grid, tile, list } final peerCardUiType = PeerUiType.grid.obs; diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index b5143eb82..d472d086f 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -215,29 +215,7 @@ class _PeerTabPageState extends State } Widget _createPeerViewTypeSwitch(BuildContext context) { - final textColor = Theme.of(context).textTheme.titleLarge?.color; - final types = [PeerUiType.grid, PeerUiType.list]; - - return Obx(() => _hoverAction( - context: context, - onTap: () async { - final type = types - .elementAt(peerCardUiType.value == types.elementAt(0) ? 1 : 0); - await bind.setLocalFlutterOption( - k: 'peer-card-ui-type', v: type.index.toString()); - peerCardUiType.value = type; - }, - child: Tooltip( - message: peerCardUiType.value == PeerUiType.grid - ? translate('List View') - : translate('Grid View'), - child: Icon( - peerCardUiType.value == PeerUiType.grid - ? Icons.view_list_rounded - : Icons.grid_view_rounded, - size: 18, - color: textColor, - )))); + return PeerViewDropdown(); } Widget _createMultiSelection() { @@ -777,6 +755,85 @@ class _PeerSearchBarState extends State { } } +class PeerViewDropdown extends StatefulWidget { + const PeerViewDropdown({super.key}); + + @override + State createState() => _PeerViewDropdownState(); +} + +class _PeerViewDropdownState extends State { + RelativeRect menuPos = RelativeRect.fromLTRB(0, 0, 0, 0); + + @override + Widget build(BuildContext context) { + final List types = [PeerUiType.grid, PeerUiType.tile, PeerUiType.list]; + final style = TextStyle( + color: Theme.of(context).textTheme.titleLarge?.color, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); + List items = List.empty(growable: true); + items.add(PopupMenuItem( + height: 36, + enabled: false, + child: Text(translate("Change view"), style: style))); + for (var e in PeerUiType.values) { + items.add(PopupMenuItem( + height: 36, + child: Obx(() => Center( + child: SizedBox( + height: 36, + child: getRadio( + Text(translate( + types.indexOf(e) == 0 ? 'Big tiles' : types.indexOf(e) == 1 ? 'Small tiles' : 'List' + ), style: style), + e, + peerCardUiType.value, + dense: true, + (PeerUiType? v) async { + if (v != null) { + peerCardUiType.value = v; + setState(() {}); + await bind.setLocalFlutterOption( + k: "peer-card-ui-type", + v: peerCardUiType.value.index.toString(), + ); + }} + ), + ), + )))); + } + + return _hoverAction( + context: context, + child: Tooltip( + message: translate('Change view'), + child: Icon( + peerCardUiType.value == PeerUiType.grid + ? Icons.grid_view_rounded + : peerCardUiType.value == PeerUiType.tile + ? Icons.view_list_rounded + : Icons.view_agenda_rounded, + size: 18, + )), + onTapDown: (details) { + final x = details.globalPosition.dx; + final y = details.globalPosition.dy; + setState(() { + menuPos = RelativeRect.fromLTRB(x, y, x, y); + }); + }, + onTap: () => showMenu( + context: context, + position: menuPos, + items: items, + elevation: 8, + ), + ); + } +} + + class PeerSortDropdown extends StatefulWidget { const PeerSortDropdown({super.key}); diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index c058dd4dc..24949b94a 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -9,6 +9,7 @@ import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:visibility_detector/visibility_detector.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:flutter_hbb/models/peer_tab_model.dart'; import '../../common.dart'; import '../../models/peer_model.dart'; @@ -188,12 +189,19 @@ class _PeersViewState extends State<_PeersView> with WindowListener { onVisibilityChanged: onVisibilityChanged, child: widget.peerCardBuilder(peer), ); + final windowWidth = MediaQuery.of(context).size.width; + final model = Provider.of(context); + final hideAbTagsPanel = bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty; return isDesktop ? Obx( () => SizedBox( - width: 220, + width: peerCardUiType.value != PeerUiType.list + ? 220 + : model.currentTab == PeerTabIndex.group.index || (model.currentTab == PeerTabIndex.ab.index && !hideAbTagsPanel) + ? windowWidth - 390 : + windowWidth - 227, height: - peerCardUiType.value == PeerUiType.grid ? 140 : 42, + peerCardUiType.value == PeerUiType.grid ? 140 : peerCardUiType.value != PeerUiType.list ? 42 : 45, child: visibilityChild, ), ) diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 27dd880e8..17c8883a5 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -224,11 +224,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { )); } // record - var codecFormat = ffi.qualityMonitorModel.data.codecFormat; if (!isDesktop && - (ffi.recordingModel.start || - (perms["recording"] != false && - (codecFormat == "VP8" || codecFormat == "VP9")))) { + (ffi.recordingModel.start || (perms["recording"] != false))) { v.add(TTextMenu( child: Row( children: [ @@ -533,5 +530,20 @@ Future> toolbarDisplayToggle( child: Text(translate('Show displays as individual windows')))); } + final screenList = await getScreenRectList(); + if (useTextureRender && pi.isSupportMultiDisplay && screenList.length > 1) { + final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession( + sessionId: ffi.sessionId) == + 'Y'; + v.add(TToggleMenu( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionSetUseAllMyDisplaysForTheRemoteSession( + sessionId: sessionId, value: value ? 'Y' : ''); + }, + child: Text(translate('Use all my displays for the remote session')))); + } + return v; } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index d92f42a10..ccd8fbaac 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/state_model.dart'; +import 'package:get/get.dart'; const double kDesktopRemoteTabBarHeight = 28.0; const int kInvalidWindowId = -1; @@ -10,6 +11,10 @@ const int kMainWindowId = 0; const kAllDisplayValue = -1; +const kKeyLegacyMode = 'legacy'; +const kKeyMapMode = 'map'; +const kKeyTranslateMode = 'translate'; + const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformMacOS = "Mac OS"; @@ -29,6 +34,7 @@ const String kAppTypeDesktopPortForward = "port forward"; const String kWindowMainWindowOnTop = "main_window_on_top"; const String kWindowGetWindowInfo = "get_window_info"; +const String kWindowGetScreenList = "get_screen_list"; const String kWindowDisableGrabKeyboard = "disable_grab_keyboard"; const String kWindowActionRebuild = "rebuild"; const String kWindowEventHide = "hide"; @@ -64,7 +70,10 @@ const int kWindowMainId = 0; const String kPointerEventKindTouch = "touch"; const String kPointerEventKindMouse = "mouse"; -const String kKeyShowDisplaysAsIndividualWindows = 'displays_as_individual_windows'; +const String kKeyShowDisplaysAsIndividualWindows = + 'displays_as_individual_windows'; +const String kKeyUseAllMyDisplaysForTheRemoteSession = + 'use_all_my_displays_for_the_remote_session'; const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar'; // the executable name of the portable version @@ -84,9 +93,17 @@ const int kDesktopMaxDisplaySize = 3840; const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferHeaderHeight = 25.0; +double kNewWindowOffset = Platform.isWindows + ? 56.0 + : Platform.isLinux + ? 50.0 + : Platform.isMacOS + ? 30.0 + : 50.0; + EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && Platform.isLinux - ? stateGlobal.fullscreen || stateGlobal.isMaximized.value + ? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value ? EdgeInsets.zero : EdgeInsets.all(5.0) : EdgeInsets.zero; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 359746f4c..a40e8abe0 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -11,10 +11,12 @@ import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; import '../../common.dart'; import '../../common/formatter/id_formatter.dart'; import '../../common/widgets/peer_tab_page.dart'; +import '../../common/widgets/autocomplete.dart'; import '../../models/platform_model.dart'; import '../widgets/button.dart'; @@ -35,12 +37,21 @@ class _ConnectionPageState extends State Timer? _updateTimer; final RxBool _idInputFocused = false.obs; - final FocusNode _idFocusNode = FocusNode(); var svcStopped = Get.find(tag: 'stop-service'); var svcIsUsingPublicServer = true.obs; bool isWindowMinimized = false; + List peers = []; + List _frontN(List list, int n) { + if (list.length <= n) { + return list; + } else { + return list.sublist(0, n); + } + } + bool isPeersLoading = false; + bool isPeersLoaded = false; @override void initState() { @@ -58,12 +69,6 @@ class _ConnectionPageState extends State _updateTimer = periodic_immediate(Duration(seconds: 1), () async { updateStatus(); }); - _idFocusNode.addListener(() { - _idInputFocused.value = _idFocusNode.hasFocus; - // select all to faciliate removing text, just following the behavior of address input of chrome - _idController.selection = TextSelection( - baseOffset: 0, extentOffset: _idController.value.text.length); - }); Get.put(_idController); windowManager.addListener(this); } @@ -76,6 +81,9 @@ class _ConnectionPageState extends State if (Get.isRegistered()) { Get.delete(); } + if (Get.isRegistered()){ + Get.delete(); + } super.dispose(); } @@ -142,8 +150,20 @@ class _ConnectionPageState extends State connect(context, id, isFileTransfer: isFileTransfer); } + Future _fetchPeers() async { + setState(() { + isPeersLoading = true; + }); + await Future.delayed(Duration(milliseconds: 100)); + peers = await getAllPeers(); + setState(() { + isPeersLoading = false; + isPeersLoaded = true; + }); + } + /// UI for the remote ID TextField. - /// Search for a peer and connect to it if the id exists. + /// Search for a peer. Widget _buildRemoteIDTextField(BuildContext context) { var w = Container( width: 320 + 20 * 2, @@ -171,36 +191,127 @@ class _ConnectionPageState extends State Row( children: [ Expanded( - child: Obx( - () => TextField( - maxLength: 90, - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - focusNode: _idFocusNode, - style: const TextStyle( - fontFamily: 'WorkSans', - fontSize: 22, - height: 1.4, - ), - maxLines: 1, - cursorColor: - Theme.of(context).textTheme.titleLarge?.color, - decoration: InputDecoration( - filled: false, - counterText: '', - hintText: _idInputFocused.value - ? null - : translate('Enter Remote ID'), - contentPadding: const EdgeInsets.symmetric( - horizontal: 15, vertical: 13)), - controller: _idController, - inputFormatters: [IDTextInputFormatter()], - onSubmitted: (s) { - onConnect(); - }, - ), - ), + child: + Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text == '') { + return const Iterable.empty(); + } + else if (peers.isEmpty && !isPeersLoaded) { + Peer emptyPeer = Peer( + id: '', + username: '', + hostname: '', + alias: '', + platform: '', + tags: [], + hash: '', + forceAlwaysRelay: false, + rdpPort: '', + rdpUsername: '', + loginName: '', + ); + return [emptyPeer]; + } + else { + String textWithoutSpaces = textEditingValue.text.replaceAll(" ", ""); + if (int.tryParse(textWithoutSpaces) != null) { + textEditingValue = TextEditingValue( + text: textWithoutSpaces, + selection: textEditingValue.selection, + ); + } + String textToFind = textEditingValue.text.toLowerCase(); + + return peers.where((peer) => + peer.id.toLowerCase().contains(textToFind) || + peer.username.toLowerCase().contains(textToFind) || + peer.hostname.toLowerCase().contains(textToFind) || + peer.alias.toLowerCase().contains(textToFind)) + .toList(); + } + }, + + fieldViewBuilder: (BuildContext context, + TextEditingController fieldTextEditingController, + FocusNode fieldFocusNode , + VoidCallback onFieldSubmitted, + ) { + fieldTextEditingController.text = _idController.text; + Get.put(fieldTextEditingController); + fieldFocusNode.addListener(() async { + _idInputFocused.value = fieldFocusNode.hasFocus; + if (fieldFocusNode.hasFocus && !isPeersLoading){ + _fetchPeers(); + } + }); + final textLength = fieldTextEditingController.value.text.length; + // select all to facilitate removing text, just following the behavior of address input of chrome + fieldTextEditingController.selection = TextSelection(baseOffset: 0, extentOffset: textLength); + return Obx(() => + TextField( + maxLength: 90, + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + focusNode: fieldFocusNode, + style: const TextStyle( + fontFamily: 'WorkSans', + fontSize: 22, + height: 1.4, + ), + maxLines: 1, + cursorColor: Theme.of(context).textTheme.titleLarge?.color, + decoration: InputDecoration( + filled: false, + counterText: '', + hintText: _idInputFocused.value + ? null + : translate('Enter Remote ID'), + contentPadding: const EdgeInsets.symmetric( + horizontal: 15, vertical: 13)), + controller: fieldTextEditingController, + inputFormatters: [IDTextInputFormatter()], + onChanged: (v) { + _idController.id = v; + }, + )); + }, + optionsViewBuilder: (BuildContext context, AutocompleteOnSelected onSelected, Iterable options) { + double maxHeight = options.length * 50; + maxHeight = maxHeight > 200 ? 200 : maxHeight; + + return Align( + alignment: Alignment.topLeft, + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Material( + elevation: 4, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: maxHeight, + maxWidth: 319, + ), + child: peers.isEmpty && isPeersLoading + ? Container( + height: 80, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + ) + : Padding( + padding: const EdgeInsets.only(top: 5), + child: ListView( + children: options.map((peer) => AutocompletePeerTile(idController: _idController, peer: peer)).toList(), + ), + ), + ), + )), + ); + }, + ) ), ], ), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 8ea872c6c..59118f7cd 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -329,8 +329,7 @@ class _DesktopHomePageState extends State "Click to download", () async { final Uri url = Uri.parse('https://rustdesk.com/download'); await launchUrl(url); - }, - closeButton: true); + }, closeButton: true); } if (systemError.isNotEmpty) { return buildInstallCard("", systemError, "", () {}); @@ -379,16 +378,39 @@ class _DesktopHomePageState extends State // }); // } } else if (Platform.isLinux) { + final LinuxCards = []; + if (bind.isSelinuxEnforcing()) { + // Check is SELinux enforcing, but show user a tip of is SELinux enabled for simple. + final keyShowSelinuxHelpTip = "show-selinux-help-tip"; + if (bind.mainGetLocalOption(key: keyShowSelinuxHelpTip) != 'N') { + LinuxCards.add(buildInstallCard( + "Warning", "selinux_tip", "", () async {}, + marginTop: LinuxCards.isEmpty ? 20.0 : 5.0, + help: 'Help', + link: + 'https://rustdesk.com/docs/en/client/linux/#permissions-issue', + closeButton: true, + closeOption: keyShowSelinuxHelpTip, + )); + } + } if (bind.mainCurrentIsWayland()) { - return buildInstallCard( + LinuxCards.add(buildInstallCard( "Warning", "wayland_experiment_tip", "", () async {}, + marginTop: LinuxCards.isEmpty ? 20.0 : 5.0, help: 'Help', - link: 'https://rustdesk.com/docs/en/manual/linux/#x11-required'); + link: 'https://rustdesk.com/docs/en/manual/linux/#x11-required')); } else if (bind.mainIsLoginWayland()) { - return buildInstallCard("Warning", + LinuxCards.add(buildInstallCard("Warning", "Login screen using Wayland is not supported", "", () async {}, + marginTop: LinuxCards.isEmpty ? 20.0 : 5.0, help: 'Help', - link: 'https://rustdesk.com/docs/en/manual/linux/#login-screen'); + link: 'https://rustdesk.com/docs/en/manual/linux/#login-screen')); + } + if (LinuxCards.isNotEmpty) { + return Column( + children: LinuxCards, + ); } } return Container(); @@ -396,18 +418,26 @@ class _DesktopHomePageState extends State Widget buildInstallCard(String title, String content, String btnText, GestureTapCallback onPressed, - {String? help, String? link, bool? closeButton}) { - - void closeCard() { - setState(() { - isCardClosed = true; - }); + {double marginTop = 20.0, String? help, String? link, bool? closeButton, String? closeOption}) { + void closeCard() async { + if (closeOption != null) { + await bind.mainSetLocalOption(key: closeOption, value: 'N'); + if (bind.mainGetLocalOption(key: closeOption) == 'N') { + setState(() { + isCardClosed = true; + }); + } + } else { + setState(() { + isCardClosed = true; + }); + } } return Stack( children: [ Container( - margin: EdgeInsets.only(top: 20), + margin: EdgeInsets.only(top: marginTop), child: Container( decoration: BoxDecoration( gradient: LinearGradient( @@ -555,6 +585,22 @@ class _DesktopHomePageState extends State Get.put(svcStopped, tag: 'stop-service'); rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); + screenToMap(window_size.Screen screen) => { + 'frame': { + 'l': screen.frame.left, + 't': screen.frame.top, + 'r': screen.frame.right, + 'b': screen.frame.bottom, + }, + 'visibleFrame': { + 'l': screen.visibleFrame.left, + 't': screen.visibleFrame.top, + 'r': screen.visibleFrame.right, + 'b': screen.visibleFrame.bottom, + }, + 'scaleFactor': screen.scaleFactor, + }; + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { debugPrint( "[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId"); @@ -563,24 +609,13 @@ class _DesktopHomePageState extends State } else if (call.method == kWindowGetWindowInfo) { final screen = (await window_size.getWindowInfo()).screen; if (screen == null) { - return ""; + return ''; } else { - return jsonEncode({ - 'frame': { - 'l': screen.frame.left, - 't': screen.frame.top, - 'r': screen.frame.right, - 'b': screen.frame.bottom, - }, - 'visibleFrame': { - 'l': screen.visibleFrame.left, - 't': screen.visibleFrame.top, - 'r': screen.visibleFrame.right, - 'b': screen.visibleFrame.bottom, - }, - 'scaleFactor': screen.scaleFactor, - }); + return jsonEncode(screenToMap(screen)); } + } else if (call.method == kWindowGetScreenList) { + return jsonEncode( + (await window_size.getScreenList()).map(screenToMap).toList()); } else if (call.method == kWindowActionRebuild) { reloadCurrentWindow(); } else if (call.method == kWindowEventShow) { @@ -613,8 +648,9 @@ class _DesktopHomePageState extends State final peerId = args['peer_id'] as String; final display = args['display'] as int; final displayCount = args['display_count'] as int; + final screenRect = parseParamScreenRect(args); await rustDeskWinManager.openMonitorSession( - windowId, peerId, display, displayCount); + windowId, peerId, display, displayCount, screenRect); } }); _uniLinksSubscription = listenUniLinks(); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index f74535047..416cbaa5e 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1324,6 +1324,8 @@ class _DisplayState extends State<_Display> { if (useTextureRender) { children.add(otherRow('Show displays as individual windows', kKeyShowDisplaysAsIndividualWindows)); + children.add(otherRow('Use all my displays for the remote session', + kKeyUseAllMyDisplaysForTheRemoteSession)); } return _Card(title: 'Other Default Options', children: children); } diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index abbb8785d..ffc29b02f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -80,7 +80,7 @@ class _RemotePageState extends State late RxBool _keyboardEnabled; final Map _renderTextures = {}; - final _blockableOverlayState = BlockableOverlayState(); + var _blockableOverlayState = BlockableOverlayState(); final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); @@ -253,9 +253,9 @@ class _RemotePageState extends State onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null, setRemoteState: setState, ); - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, - body: Stack( + + bodyWidget() { + return Stack( children: [ Container( color: Colors.black, @@ -281,7 +281,7 @@ class _RemotePageState extends State }, inputModel: _ffi.inputModel, child: getBodyForDesktop(context))), - Obx(() => Stack( + Stack( children: [ _ffi.ffiModel.pi.isSet.isTrue && _ffi.ffiModel.waitForFirstImage.isTrue @@ -298,9 +298,34 @@ class _RemotePageState extends State : remoteToolbar(context), _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(), ], - )), + ), ], - ), + ); + } + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: Obx(() { + final imageReady = _ffi.ffiModel.pi.isSet.isTrue && + _ffi.ffiModel.waitForFirstImage.isFalse; + if (imageReady) { + // `dismissAll()` is to ensure that the state is clean. + // It's ok to call dismissAll() here. + _ffi.dialogManager.dismissAll(); + // Recreate the block state to refresh the state. + _blockableOverlayState = BlockableOverlayState(); + _blockableOverlayState.applyFfi(_ffi); + // Block the whole `bodyWidget()` when dialog shows. + return BlockableOverlay( + underlying: bodyWidget(), + state: _blockableOverlayState, + ); + } else { + // `_blockableOverlayState` is not recreated here. + // The toolbar's block state won't work properly when reconnecting, but that's okay. + return bodyWidget(); + } + }), ); } @@ -677,7 +702,8 @@ class _ImagePaintState extends State { } else { final key = cache.updateGetKey(scale); if (!cursor.cachedKeys.contains(key)) { - debugPrint("Register custom cursor with key $key (${cache.hotx},${cache.hoty})"); + debugPrint( + "Register custom cursor with key $key (${cache.hotx},${cache.hoty})"); // [Safety] // It's ok to call async registerCursor in current synchronous context, // because activating the cursor is also an async call and will always diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 0cc23b39a..3b56ef4cc 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -48,6 +48,8 @@ class _ConnectionTabPageState extends State { late ToolbarState _toolbarState; String? peerId; + bool _isScreenRectSet = false; + int? _display; var connectionMap = RxList.empty(growable: true); @@ -59,6 +61,10 @@ class _ConnectionTabPageState extends State { final tabWindowId = params['tab_window_id']; final display = params['display']; final displays = params['displays']; + final screenRect = parseParamScreenRect(params); + _isScreenRectSet = screenRect != null; + _display = display as int?; + tryMoveToScreenAndSetFullscreen(screenRect); if (peerId != null) { ConnectionTypeState.init(peerId!); tabController.onSelected = (id) { @@ -115,11 +121,16 @@ class _ConnectionTabPageState extends State { final tabWindowId = args['tab_window_id']; final display = args['display']; final displays = args['displays']; + final screenRect = parseParamScreenRect(args); windowOnTop(windowId()); + tryMoveToScreenAndSetFullscreen(screenRect); if (tabController.length == 0) { - if (Platform.isMacOS && stateGlobal.closeOnFullscreen) { + // Show the hidden window. + if (Platform.isMacOS && stateGlobal.closeOnFullscreen == true) { stateGlobal.setFullscreen(true); } + // Reset the state + stateGlobal.closeOnFullscreen = null; } ConnectionTypeState.init(id); _toolbarState.setShow( @@ -196,15 +207,18 @@ class _ConnectionTabPageState extends State { _update_remote_count(); return returnValue; }); - Future.delayed(Duration.zero, () { - restoreWindowPosition( - WindowType.RemoteDesktop, - windowId: windowId(), - peerId: tabController.state.value.tabs.isEmpty - ? null - : tabController.state.value.tabs[0].key, - ); - }); + if (!_isScreenRectSet) { + Future.delayed(Duration.zero, () { + restoreWindowPosition( + WindowType.RemoteDesktop, + windowId: windowId(), + peerId: tabController.state.value.tabs.isEmpty + ? null + : tabController.state.value.tabs[0].key, + display: _display, + ); + }); + } } @override @@ -451,6 +465,7 @@ class _ConnectionTabPageState extends State { c++; } } + loopCloseWindow(); } ConnectionTypeState.delete(id); diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 92038f68b..1f161ec42 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1,12 +1,10 @@ import 'dart:convert'; import 'dart:async'; import 'dart:io'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/toolbar.dart'; -import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/desktop_render_texture.dart'; @@ -22,17 +20,12 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:window_size/window_size.dart' as window_size; import '../../common.dart'; -import '../../common/widgets/dialog.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import './popup_menu.dart'; import './kb_layout_type_chooser.dart'; -const _kKeyLegacyMode = 'legacy'; -const _kKeyMapMode = 'map'; -const _kKeyTranslateMode = 'translate'; - class ToolbarState { final kStoreKey = 'remoteMenubarState'; late RxBool show; @@ -353,10 +346,10 @@ class _RemoteToolbarState extends State { int get windowId => stateGlobal.windowId; - bool get isFullscreen => stateGlobal.fullscreen; void _setFullscreen(bool v) { stateGlobal.setFullscreen(v); - setState(() {}); + // stateGlobal.fullscreen is RxBool now, no need to call setState. + // setState(() {}); } RxBool get show => widget.state.show; @@ -480,7 +473,7 @@ class _RemoteToolbarState extends State { toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); } - toolbarItems.add(_RecordMenu(ffi: widget.ffi)); + toolbarItems.add(_RecordMenu()); toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi)); return Column( mainAxisSize: MainAxisSize.min, @@ -744,42 +737,14 @@ class _MonitorMenu extends StatelessWidget { ); } - // Open new tab or window to show this monitor. - // For now just open new window. - openMonitorInNewTabOrWindow(int i, PeerInfo pi) { - if (kWindowId == null) { - // unreachable - debugPrint('openMonitorInNewTabOrWindow, unreachable! kWindowId is null'); - return; - } - DesktopMultiWindow.invokeMethod( - kMainWindowId, - kWindowEventOpenMonitorSession, - jsonEncode({ - 'window_id': kWindowId!, - 'peer_id': ffi.id, - 'display': i, - 'display_count': pi.displays.length, - })); - } - - openMonitorInTheSameTab(int i, PeerInfo pi) { - final displays = i == kAllDisplayValue - ? List.generate(pi.displays.length, (index) => index) - : [i]; - bind.sessionSwitchDisplay( - sessionId: ffi.sessionId, value: Int32List.fromList(displays)); - ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, id); - } - onPressed(int i, PeerInfo pi) { _menuDismissCallback(ffi); RxInt display = CurrentDisplayState.find(id); if (display.value != i) { if (isChooseDisplayToOpenInNewWindow(pi, ffi.sessionId)) { - openMonitorInNewTabOrWindow(i, pi); + openMonitorInNewTabOrWindow(i, ffi.id, pi); } else { - openMonitorInTheSameTab(i, pi); + openMonitorInTheSameTab(i, ffi, pi); } } } @@ -827,7 +792,7 @@ class ScreenAdjustor { required this.cbExitFullscreen, }); - bool get isFullscreen => stateGlobal.fullscreen; + bool get isFullscreen => stateGlobal.fullscreen.isTrue; int get windowId => stateGlobal.windowId; adjustWindow(BuildContext context) { @@ -981,7 +946,6 @@ class _DisplayMenuState extends State<_DisplayMenu> { cbExitFullscreen: () => widget.setFullscreen(false), ); - bool get isFullscreen => stateGlobal.fullscreen; int get windowId => stateGlobal.windowId; Map get perms => widget.ffi.ffiModel.permissions; PeerInfo get pi => widget.ffi.ffiModel.pi; @@ -1438,18 +1402,16 @@ class _KeyboardMenu extends StatelessWidget { Widget build(BuildContext context) { var ffiModel = Provider.of(context); if (!ffiModel.keyboard) return Offstage(); + // If use flutter to grab keys, we can only use one mode. + // Map mode and Legacy mode, at least one of them is supported. String? modeOnly; if (stateGlobal.grabKeyboard) { if (bind.sessionIsKeyboardModeSupported( - sessionId: ffi.sessionId, mode: _kKeyMapMode)) { - bind.sessionSetKeyboardMode( - sessionId: ffi.sessionId, value: _kKeyMapMode); - modeOnly = _kKeyMapMode; + sessionId: ffi.sessionId, mode: kKeyMapMode)) { + modeOnly = kKeyMapMode; } else if (bind.sessionIsKeyboardModeSupported( - sessionId: ffi.sessionId, mode: _kKeyLegacyMode)) { - bind.sessionSetKeyboardMode( - sessionId: ffi.sessionId, value: _kKeyLegacyMode); - modeOnly = _kKeyLegacyMode; + sessionId: ffi.sessionId, mode: kKeyLegacyMode)) { + modeOnly = kKeyLegacyMode; } } return _IconSubmenuButton( @@ -1471,13 +1433,13 @@ class _KeyboardMenu extends StatelessWidget { keyboardMode(String? modeOnly) { return futureBuilder(future: () async { return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ?? - _kKeyLegacyMode; + kKeyLegacyMode; }(), hasData: (data) { final groupValue = data as String; List modes = [ - InputModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'), - InputModeMenu(key: _kKeyMapMode, menu: 'Map mode'), - InputModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'), + InputModeMenu(key: kKeyLegacyMode, menu: 'Legacy mode'), + InputModeMenu(key: kKeyMapMode, menu: 'Map mode'), + InputModeMenu(key: kKeyTranslateMode, menu: 'Translate mode'), ]; List list = []; final enabled = !ffi.ffiModel.viewOnly; @@ -1495,12 +1457,12 @@ class _KeyboardMenu extends StatelessWidget { continue; } - if (pi.isWayland && mode.key != _kKeyMapMode) { + if (pi.isWayland && mode.key != kKeyMapMode) { continue; } var text = translate(mode.menu); - if (mode.key == _kKeyTranslateMode) { + if (mode.key == kKeyTranslateMode) { text = '$text beta'; } list.add(RdoMenuButton( @@ -1677,17 +1639,17 @@ class _VoiceCallMenu extends StatelessWidget { } class _RecordMenu extends StatelessWidget { - final FFI ffi; - const _RecordMenu({Key? key, required this.ffi}) : super(key: key); + const _RecordMenu({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - var ffiModel = Provider.of(context); + var ffi = Provider.of(context); var recordingModel = Provider.of(context); final visible = - recordingModel.start || ffiModel.permissions['recording'] != false; + (recordingModel.start || ffi.permissions['recording'] != false) && + ffi.pi.currentDisplay != kAllDisplayValue; if (!visible) return Offstage(); - final menuButton = _IconMenuButton( + return _IconMenuButton( assetName: 'assets/rec.svg', tooltip: recordingModel.start ? 'Stop session recording' @@ -1700,14 +1662,6 @@ class _RecordMenu extends StatelessWidget { ? _ToolbarTheme.hoverRedColor : _ToolbarTheme.hoverBlueColor, ); - return ChangeNotifierProvider.value( - value: ffi.qualityMonitorModel, - child: Consumer( - builder: (context, model, child) => Offstage( - // If already started, AV1->Hidden/Stop, Other->Start, same as actual - offstage: model.data.codecFormat == 'AV1', - child: menuButton, - ))); } } @@ -1722,7 +1676,7 @@ class _CloseMenu extends StatelessWidget { return _IconMenuButton( assetName: 'assets/close.svg', tooltip: 'Close', - onPressed: () => clientClose(ffi.sessionId, ffi.dialogManager), + onPressed: () => closeConnection(id: id), color: _ToolbarTheme.redColor, hoverColor: _ToolbarTheme.hoverRedColor, ); @@ -2090,32 +2044,34 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { mainAxisSize: MainAxisSize.min, children: [ _buildDraggable(context), - TextButton( - onPressed: () { - widget.setFullscreen(!isFullscreen); - setState(() {}); - }, - child: Tooltip( - message: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), - child: Icon( - isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen, - size: iconSize, - ), - ), - ), - Offstage( - offstage: !isFullscreen, - child: TextButton( - onPressed: () => widget.setMinimize(), - child: Tooltip( - message: translate('Minimize'), - child: Icon( - Icons.remove, - size: iconSize, + Obx(() => TextButton( + onPressed: () { + widget.setFullscreen(!isFullscreen.value); + }, + child: Tooltip( + message: translate( + isFullscreen.isTrue ? 'Exit Fullscreen' : 'Fullscreen'), + child: Icon( + isFullscreen.isTrue + ? Icons.fullscreen_exit + : Icons.fullscreen, + size: iconSize, + ), ), - ), - ), - ), + )), + Obx(() => Offstage( + offstage: isFullscreen.isFalse, + child: TextButton( + onPressed: () => widget.setMinimize(), + child: Tooltip( + message: translate('Minimize'), + child: Icon( + Icons.remove, + size: iconSize, + ), + ), + ), + )), TextButton( onPressed: () => setState(() { widget.show.value = !widget.show.value; diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 5ce5601a0..7f1449ca4 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -448,6 +448,7 @@ class DesktopTab extends StatelessWidget { isMainWindow: isMainWindow, tabType: tabType, state: state, + tabController: controller, tail: tail, showMinimize: showMinimize, showMaximize: showMaximize, @@ -463,6 +464,7 @@ class WindowActionPanel extends StatefulWidget { final bool isMainWindow; final DesktopTabType tabType; final Rx state; + final DesktopTabController tabController; final bool showMinimize; final bool showMaximize; @@ -475,6 +477,7 @@ class WindowActionPanel extends StatefulWidget { required this.isMainWindow, required this.tabType, required this.state, + required this.tabController, this.tail, this.showMinimize = true, this.showMaximize = true, @@ -580,19 +583,38 @@ class WindowActionPanelState extends State void onWindowClose() async { mainWindowClose() async => await windowManager.hide(); notMainWindowClose(WindowController controller) async { - await controller.hide(); - await Future.wait([ - rustDeskWinManager - .call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}), - widget.onClose?.call() ?? Future.microtask(() => null) - ]); + if (widget.tabController.length == 0) { + debugPrint("close emtpy multiwindow, hide"); + await controller.hide(); + await rustDeskWinManager + .call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}); + } else { + debugPrint("close not emtpy multiwindow from taskbar"); + if (Platform.isWindows) { + await controller.show(); + await controller.focus(); + final res = await widget.onClose?.call() ?? true; + if (res) { + Future.delayed(Duration.zero, () async { + // onWindowClose will be called again to hide + await WindowController.fromWindowId(kWindowId!).close(); + }); + } + } else { + // ubuntu22.04 windowOnTop not work from taskbar + widget.tabController.clear(); + Future.delayed(Duration.zero, () async { + // onWindowClose will be called again to hide + await WindowController.fromWindowId(kWindowId!).close(); + }); + } + } } macOSWindowClose( - Future Function() restoreFunc, - Future Function() checkFullscreen, - Future Function() closeFunc) async { - await restoreFunc(); + Future Function() checkFullscreen, + Future Function() closeFunc, + ) async { _macOSCheckRestoreCounter = 0; _macOSCheckRestoreTimer = Timer.periodic(Duration(milliseconds: 30), (timer) async { @@ -612,26 +634,38 @@ class WindowActionPanelState extends State } // macOS specific workaround, the window is not hiding when in fullscreen. if (Platform.isMacOS && await windowManager.isFullScreen()) { - stateGlobal.closeOnFullscreen = true; + stateGlobal.closeOnFullscreen ??= true; + await windowManager.setFullScreen(false); await macOSWindowClose( - () async => await windowManager.setFullScreen(false), - () async => await windowManager.isFullScreen(), - mainWindowClose); + () async => await windowManager.isFullScreen(), + mainWindowClose, + ); } else { - stateGlobal.closeOnFullscreen = false; + stateGlobal.closeOnFullscreen ??= false; await mainWindowClose(); } } else { // it's safe to hide the subwindow final controller = WindowController.fromWindowId(kWindowId!); - if (Platform.isMacOS && await controller.isFullScreen()) { - stateGlobal.closeOnFullscreen = true; - await macOSWindowClose( - () async => await controller.setFullscreen(false), - () async => await controller.isFullScreen(), - () async => await notMainWindowClose(controller)); + if (Platform.isMacOS) { + // onWindowClose() maybe called multiple times because of loopCloseWindow() in remote_tab_page.dart. + // use ??= to make sure the value is set on first call. + + if (await widget.onClose?.call() ?? true) { + if (await controller.isFullScreen()) { + stateGlobal.closeOnFullscreen ??= true; + await controller.setFullscreen(false); + stateGlobal.setFullscreen(false, procWnd: false); + await macOSWindowClose( + () async => await controller.isFullScreen(), + () async => await notMainWindowClose(controller), + ); + } else { + stateGlobal.closeOnFullscreen ??= false; + await notMainWindowClose(controller); + } + } } else { - stateGlobal.closeOnFullscreen = false; await notMainWindowClose(controller); } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index ef44a3bcc..d7dd5acee 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -198,8 +198,16 @@ void runMultiWindow( } switch (appType) { case kAppTypeDesktopRemote: - await restoreWindowPosition(WindowType.RemoteDesktop, - windowId: kWindowId!, peerId: argument['id'] as String?); + // If screen rect is set, the window will be moved to the target screen and then set fullscreen. + if (argument['screen_rect'] == null) { + // display can be used to control the offset of the window. + await restoreWindowPosition( + WindowType.RemoteDesktop, + windowId: kWindowId!, + peerId: argument['id'] as String?, + display: argument['display'] as int?, + ); + } break; case kAppTypeDesktopFileTransfer: await restoreWindowPosition(WindowType.FileTransfer, diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index c9bd15709..1c1dec8fc 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -6,10 +6,12 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; import '../../common.dart'; import '../../common/widgets/login.dart'; import '../../common/widgets/peer_tab_page.dart'; +import '../../common/widgets/autocomplete.dart'; import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; @@ -42,6 +44,16 @@ class _ConnectionPageState extends State { /// Update url. If it's not null, means an update is available. var _updateUrl = ''; + List peers = []; + List _frontN(List list, int n) { + if (list.length <= n) { + return list; + } else { + return list.sublist(0, n); + } + } + bool isPeersLoading = false; + bool isPeersLoaded = false; @override void initState() { @@ -116,6 +128,18 @@ class _ConnectionPageState extends State { color: Colors.white, fontWeight: FontWeight.bold)))); } + Future _fetchPeers() async { + setState(() { + isPeersLoading = true; + }); + await Future.delayed(Duration(milliseconds: 100)); + peers = await getAllPeers(); + setState(() { + isPeersLoading = false; + isPeersLoaded = true; + }); + } + /// UI for the remote ID TextField. /// Search for a peer and connect to it if the id exists. Widget _buildRemoteIDTextField() { @@ -133,12 +157,69 @@ class _ConnectionPageState extends State { Expanded( child: Container( padding: const EdgeInsets.only(left: 16, right: 16), - child: AutoSizeTextField( + child: Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + if (textEditingValue.text == '') { + return const Iterable.empty(); + } + else if (peers.isEmpty && !isPeersLoaded) { + Peer emptyPeer = Peer( + id: '', + username: '', + hostname: '', + alias: '', + platform: '', + tags: [], + hash: '', + forceAlwaysRelay: false, + rdpPort: '', + rdpUsername: '', + loginName: '', + ); + return [emptyPeer]; + } + else { + String textWithoutSpaces = textEditingValue.text.replaceAll(" ", ""); + if (int.tryParse(textWithoutSpaces) != null) { + textEditingValue = TextEditingValue( + text: textWithoutSpaces, + selection: textEditingValue.selection, + ); + } + String textToFind = textEditingValue.text.toLowerCase(); + + return peers.where((peer) => + peer.id.toLowerCase().contains(textToFind) || + peer.username.toLowerCase().contains(textToFind) || + peer.hostname.toLowerCase().contains(textToFind) || + peer.alias.toLowerCase().contains(textToFind)) + .toList(); + } + }, + fieldViewBuilder: (BuildContext context, + TextEditingController fieldTextEditingController, + FocusNode fieldFocusNode, VoidCallback onFieldSubmitted) { + fieldTextEditingController.text = _idController.text; + fieldFocusNode.addListener(() async{ + _idEmpty.value = fieldTextEditingController.text.isEmpty; + if (fieldFocusNode.hasFocus && !isPeersLoading){ + _fetchPeers(); + } + }); + final textLength = fieldTextEditingController.value.text.length; + // select all to facilitate removing text, just following the behavior of address input of chrome + fieldTextEditingController.selection = TextSelection(baseOffset: 0, extentOffset: textLength); + return AutoSizeTextField( + controller: fieldTextEditingController, + focusNode: fieldFocusNode, minFontSize: 18, autocorrect: false, enableSuggestions: false, keyboardType: TextInputType.visiblePassword, // keyboardType: TextInputType.number, + onChanged: (String text) { + _idController.id = text; + }, style: const TextStyle( fontFamily: 'WorkSans', fontWeight: FontWeight.bold, @@ -161,8 +242,36 @@ class _ConnectionPageState extends State { color: MyTheme.darkGray, ), ), - controller: _idController, inputFormatters: [IDTextInputFormatter()], + ); + }, + optionsViewBuilder: (BuildContext context, AutocompleteOnSelected onSelected, Iterable options) { + double maxHeight = options.length * 50; + maxHeight = maxHeight > 200 ? 200 : maxHeight; + return Align( + alignment: Alignment.topLeft, + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Material( + elevation: 4, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: maxHeight, + maxWidth: 320, + ), + child: peers.isEmpty && isPeersLoading + ? Container( + height: 80, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + ))) + : ListView( + padding: EdgeInsets.only(top: 5), + children: options.map((peer) => AutocompletePeerTile(idController: _idController, peer: peer)).toList(), + )))) + ); + }, ), ), ), @@ -170,7 +279,9 @@ class _ConnectionPageState extends State { offstage: _idEmpty.value, child: IconButton( onPressed: () { - _idController.clear(); + setState(() { + _idController.clear(); + }); }, icon: Icon(Icons.clear, color: MyTheme.darkGray)), )), diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 249355012..83a3f77c5 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -235,7 +235,7 @@ class _RemotePageState extends State { clientClose(sessionId, gFFI.dialogManager); return false; }, - child: getRawPointerAndKeyBody(Scaffold( + child: Scaffold( // workaround for https://github.com/rustdesk/rustdesk/issues/3131 floatingActionButtonLocation: keyboardIsVisible ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) @@ -281,7 +281,7 @@ class _RemotePageState extends State { : Offstage(), ], )), - body: Overlay( + body: getRawPointerAndKeyBody(Overlay( initialEntries: [ OverlayEntry(builder: (context) { return Container( @@ -763,7 +763,9 @@ void showOptions( children.add(InkWell( onTap: () { if (i == cur) return; - bind.sessionSwitchDisplay(sessionId: gFFI.sessionId, value: Int32List.fromList([i])); + gFFI.recordingModel.onClose(); + bind.sessionSwitchDisplay( + sessionId: gFFI.sessionId, value: Int32List.fromList([i])); gFFI.dialogManager.dismissAll(); }, child: Ink( diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 77eeb3a7e..93cdbbed5 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -103,7 +103,7 @@ class ChatModel with ChangeNotifier { void setOverlayState(BlockableOverlayState blockableOverlayState) { _blockableOverlayState = blockableOverlayState; - _blockableOverlayState!.addMiddleBlockedListener((v) { + _blockableOverlayState.addMiddleBlockedListener((v) { if (!v) { isWindowFocus.value = false; if (isWindowFocus.value) { @@ -197,9 +197,9 @@ class ChatModel with ChangeNotifier { showChatWindowOverlay({Offset? chatInitPos}) { if (chatWindowOverlayEntry != null) return; isWindowFocus.value = true; - _blockableOverlayState?.setMiddleBlocked(true); + _blockableOverlayState.setMiddleBlocked(true); - final overlayState = _blockableOverlayState?.state; + final overlayState = _blockableOverlayState.state; if (overlayState == null) return; if (isMobile && !gFFI.chatModel.currentKey.isOut && // not in remote page @@ -212,7 +212,7 @@ class ChatModel with ChangeNotifier { onPointerDown: (_) { if (!isWindowFocus.value) { isWindowFocus.value = true; - _blockableOverlayState?.setMiddleBlocked(true); + _blockableOverlayState.setMiddleBlocked(true); } }, child: DraggableChatWindow( @@ -228,7 +228,7 @@ class ChatModel with ChangeNotifier { hideChatWindowOverlay() { if (chatWindowOverlayEntry != null) { - _blockableOverlayState?.setMiddleBlocked(false); + _blockableOverlayState.setMiddleBlocked(false); chatWindowOverlayEntry!.remove(); chatWindowOverlayEntry = null; return; diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 108c76f1e..ae1d9b701 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -261,6 +261,7 @@ class FileController { required this.getOtherSideDirectoryData}); String get homePath => options.value.home; + void set homePath(String path) => options.value.home = path; OverlayDialogManager? get dialogManager => rootState.target?.dialogManager; String get shortPath { @@ -376,6 +377,11 @@ class FileController { } void goToHomeDirectory() { + if (isLocal) { + openDirectory(homePath); + return; + } + homePath = ""; openDirectory(homePath); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d257f3290..c8675dfa7 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -227,7 +227,7 @@ class FfiModel with ChangeNotifier { }, sessionId, peerId); updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId); setConnectionType(peerId, data.secure, data.direct); - await handlePeerInfo(data.peerInfo, peerId); + await handlePeerInfo(data.peerInfo, peerId, true); for (final element in data.cursorDataList) { updateLastCursorId(element); await handleCursorData(element); @@ -245,7 +245,7 @@ class FfiModel with ChangeNotifier { if (name == 'msgbox') { handleMsgBox(evt, sessionId, peerId); } else if (name == 'peer_info') { - handlePeerInfo(evt, peerId); + handlePeerInfo(evt, peerId, false); } else if (name == 'sync_peer_info') { handleSyncPeerInfo(evt, sessionId, peerId); } else if (name == 'connection_ready') { @@ -430,14 +430,12 @@ class FfiModel with ChangeNotifier { Map evt, SessionID sessionId, String peerId) { final curDisplay = int.parse(evt['display']); - // The message should be handled by the another UI session. - if (isChooseDisplayToOpenInNewWindow(_pi, sessionId)) { - if (curDisplay != _pi.currentDisplay) { - return; - } - } - if (_pi.currentDisplay != kAllDisplayValue) { + if (bind.peerGetDefaultSessionsCount(id: peerId) > 1) { + if (curDisplay != _pi.currentDisplay) { + return; + } + } _pi.currentDisplay = curDisplay; } @@ -514,7 +512,9 @@ class FfiModel with ChangeNotifier { String link, bool hasRetry, OverlayDialogManager dialogManager, {bool? hasCancel}) { msgBox(sessionId, type, title, text, link, dialogManager, - hasCancel: hasCancel, reconnect: reconnect); + hasCancel: hasCancel, + reconnect: reconnect, + reconnectTimeout: hasRetry ? _reconnects : null); _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { @@ -530,6 +530,7 @@ class FfiModel with ChangeNotifier { bool forceRelay) { bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay); clearPermissions(); + dialogManager.dismissAll(); dialogManager.showLoading(translate('Connecting...'), onCancel: closeConnection); } @@ -623,7 +624,7 @@ class FfiModel with ChangeNotifier { } /// Handle the peer info event based on [evt]. - handlePeerInfo(Map evt, String peerId) async { + handlePeerInfo(Map evt, String peerId, bool isCache) async { cachedPeerData.peerInfo = evt; // recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs) @@ -689,12 +690,12 @@ class FfiModel with ChangeNotifier { sessionId: sessionId, arg: 'view-only')); } if (connType == ConnType.defaultConn) { - final platformDdditions = evt['platform_additions']; - if (platformDdditions != null && platformDdditions != '') { + final platformAdditions = evt['platform_additions']; + if (platformAdditions != null && platformAdditions != '') { try { - _pi.platformDdditions = json.decode(platformDdditions); + _pi.platformAdditions = json.decode(platformAdditions); } catch (e) { - debugPrint('Failed to decode platformDdditions $e'); + debugPrint('Failed to decode platformAdditions $e'); } } } @@ -702,7 +703,86 @@ class FfiModel with ChangeNotifier { _pi.isSet.value = true; stateGlobal.resetLastResolutionGroupValues(peerId); + if (isDesktop) { + checkDesktopKeyboardMode(); + } + notifyListeners(); + + if (!isCache) { + tryUseAllMyDisplaysForTheRemoteSession(peerId); + } + } + + checkDesktopKeyboardMode() async { + final curMode = await bind.sessionGetKeyboardMode(sessionId: sessionId); + if (curMode != null) { + if (bind.sessionIsKeyboardModeSupported( + sessionId: sessionId, mode: curMode)) { + return; + } + } + + // If current keyboard mode is not supported, change to another one. + + if (stateGlobal.grabKeyboard) { + for (final mode in [kKeyMapMode, kKeyLegacyMode]) { + if (bind.sessionIsKeyboardModeSupported( + sessionId: sessionId, mode: mode)) { + bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode); + break; + } + } + } else { + for (final mode in [kKeyMapMode, kKeyTranslateMode, kKeyLegacyMode]) { + if (bind.sessionIsKeyboardModeSupported( + sessionId: sessionId, mode: mode)) { + bind.sessionSetKeyboardMode(sessionId: sessionId, value: mode); + break; + } + } + } + } + + tryUseAllMyDisplaysForTheRemoteSession(String peerId) async { + if (bind.sessionGetUseAllMyDisplaysForTheRemoteSession( + sessionId: sessionId) != + 'Y') { + return; + } + + if (!_pi.isSupportMultiDisplay || _pi.displays.length <= 1) { + return; + } + + final screenRectList = await getScreenRectList(); + if (screenRectList.length <= 1) { + return; + } + + // to-do: peer currentDisplay is the primary display, but the primary display may not be the first display. + // local primary display also may not be the first display. + // + // 0 is assumed to be the primary display here, for now. + + // move to the first display and set fullscreen + bind.sessionSwitchDisplay( + sessionId: sessionId, value: Int32List.fromList([0])); + _pi.currentDisplay = 0; + try { + CurrentDisplayState.find(peerId).value = _pi.currentDisplay; + } catch (e) { + // + } + await tryMoveToScreenAndSetFullscreen(screenRectList[0]); + + final length = _pi.displays.length < screenRectList.length + ? _pi.displays.length + : screenRectList.length; + for (var i = 1; i < length; i++) { + openMonitorInNewTabOrWindow(i, peerId, _pi, + screenRect: screenRectList[i]); + } } tryShowAndroidActionsOverlay({int delayMSecs = 10}) { @@ -780,6 +860,7 @@ class FfiModel with ChangeNotifier { } _pi.displays = newDisplays; _pi.displaysCount.value = _pi.displays.length; + if (_pi.currentDisplay == kAllDisplayValue) { updateCurDisplay(sessionId); // to-do: What if the displays are changed? @@ -816,6 +897,8 @@ class FfiModel with ChangeNotifier { // Directly switch to the new display without waiting for the response. switchToNewDisplay(int display, SessionID sessionId, String peerId) { + // VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays. + parent.target?.recordingModel.onClose(); // no need to wait for the response pi.currentDisplay = display; updateCurDisplay(sessionId); @@ -824,7 +907,6 @@ class FfiModel with ChangeNotifier { } catch (e) { // } - parent.target?.recordingModel.onSwitchDisplay(); } updateBlockInputState(Map evt, String peerId) { @@ -1806,57 +1888,67 @@ class RecordingModel with ChangeNotifier { int? width = parent.target?.canvasModel.getDisplayWidth(); int? height = parent.target?.canvasModel.getDisplayHeight(); if (sessionId == null || width == null || height == null) return; - final currentDisplay = parent.target?.ffiModel.pi.currentDisplay; - if (currentDisplay != kAllDisplayValue) { - bind.sessionRecordScreen( - sessionId: sessionId, - start: true, - display: currentDisplay!, - width: width, - height: height); - } + final pi = parent.target?.ffiModel.pi; + if (pi == null) return; + final currentDisplay = pi.currentDisplay; + if (currentDisplay == kAllDisplayValue) return; + bind.sessionRecordScreen( + sessionId: sessionId, + start: true, + display: currentDisplay, + width: width, + height: height); } toggle() async { if (isIOS) return; final sessionId = parent.target?.sessionId; if (sessionId == null) return; + final pi = parent.target?.ffiModel.pi; + if (pi == null) return; + final currentDisplay = pi.currentDisplay; + if (currentDisplay == kAllDisplayValue) return; _start = !_start; notifyListeners(); - await bind.sessionRecordStatus(sessionId: sessionId, status: _start); + await _sendStatusMessage(sessionId, pi, _start); if (_start) { - final pi = parent.target?.ffiModel.pi; - if (pi != null) { - sessionRefreshVideo(sessionId, pi); + sessionRefreshVideo(sessionId, pi); + if (versionCmp(pi.version, '1.2.4') >= 0) { + // will not receive SwitchDisplay since 1.2.4 + onSwitchDisplay(); } } else { - final currentDisplay = parent.target?.ffiModel.pi.currentDisplay; - if (currentDisplay != kAllDisplayValue) { - bind.sessionRecordScreen( - sessionId: sessionId, - start: false, - display: currentDisplay!, - width: 0, - height: 0); - } - } - } - - onClose() { - if (isIOS) return; - final sessionId = parent.target?.sessionId; - if (sessionId == null) return; - _start = false; - final currentDisplay = parent.target?.ffiModel.pi.currentDisplay; - if (currentDisplay != kAllDisplayValue) { bind.sessionRecordScreen( sessionId: sessionId, start: false, - display: currentDisplay!, + display: currentDisplay, width: 0, height: 0); } } + + onClose() async { + if (isIOS) return; + final sessionId = parent.target?.sessionId; + if (sessionId == null) return; + if (!_start) return; + _start = false; + final pi = parent.target?.ffiModel.pi; + if (pi == null) return; + final currentDisplay = pi.currentDisplay; + if (currentDisplay == kAllDisplayValue) return; + await _sendStatusMessage(sessionId, pi, false); + bind.sessionRecordScreen( + sessionId: sessionId, + start: false, + display: currentDisplay, + width: 0, + height: 0); + } + + _sendStatusMessage(SessionID sessionId, PeerInfo pi, bool status) async { + await bind.sessionRecordStatus(sessionId: sessionId, status: status); + } } class ElevationModel with ChangeNotifier { @@ -2203,13 +2295,13 @@ class PeerInfo with ChangeNotifier { List displays = []; Features features = Features(); List resolutions = []; - Map platformDdditions = {}; + Map platformAdditions = {}; RxInt displaysCount = 0.obs; RxBool isSet = false.obs; - bool get isWayland => platformDdditions['is_wayland'] == true; - bool get isHeadless => platformDdditions['headless'] == true; + bool get isWayland => platformAdditions['is_wayland'] == true; + bool get isHeadless => platformAdditions['headless'] == true; bool get isSupportMultiDisplay => isDesktop && isSupportMultiUiSession; @@ -2237,7 +2329,7 @@ class PeerInfo with ChangeNotifier { if (currentDisplay == kAllDisplayValue) { return null; } - if (currentDisplay > 0 && currentDisplay < displays.length) { + if (currentDisplay >= 0 && currentDisplay < displays.length) { return displays[currentDisplay]; } else { return null; diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 2403a794c..c80c3551e 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -11,7 +11,7 @@ enum SvcStatus { notReady, connecting, ready } class StateGlobal { int _windowId = -1; bool grabKeyboard = false; - bool _fullscreen = false; + final RxBool _fullscreen = false.obs; bool _isMinimized = false; final RxBool isMaximized = false.obs; final RxBool _showTabBar = true.obs; @@ -20,15 +20,15 @@ class StateGlobal { final RxBool showRemoteToolBar = false.obs; final svcStatus = SvcStatus.notReady.obs; // Only used for macOS - bool closeOnFullscreen = false; + bool? closeOnFullscreen; // Use for desktop -> remote toolbar -> resolution final Map> _lastResolutionGroupValues = {}; int get windowId => _windowId; - bool get fullscreen => _fullscreen; + RxBool get fullscreen => _fullscreen; bool get isMinimized => _isMinimized; - double get tabBarHeight => fullscreen ? 0 : kDesktopRemoteTabBarHeight; + double get tabBarHeight => fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight; RxBool get showTabBar => _showTabBar; RxDouble get resizeEdgeSize => _resizeEdgeSize; RxDouble get windowBorderWidth => _windowBorderWidth; @@ -51,7 +51,7 @@ class StateGlobal { setWindowId(int id) => _windowId = id; setMaximized(bool v) { - if (!_fullscreen) { + if (!_fullscreen.isTrue) { if (isMaximized.value != v) { isMaximized.value = v; _resizeEdgeSize.value = @@ -66,29 +66,27 @@ class StateGlobal { setMinimized(bool v) => _isMinimized = v; setFullscreen(bool v, {bool procWnd = true}) { - if (_fullscreen != v) { - _fullscreen = v; - _showTabBar.value = !_fullscreen; - _resizeEdgeSize.value = fullscreen + if (_fullscreen.value != v) { + _fullscreen.value = v; + _showTabBar.value = !_fullscreen.value; + _resizeEdgeSize.value = fullscreen.isTrue ? kFullScreenEdgeSize : isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize; print( "fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}"); - _windowBorderWidth.value = fullscreen ? 0 : kWindowBorderWidth; + _windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth; if (procWnd) { - WindowController.fromWindowId(windowId) - .setFullscreen(_fullscreen) - .then((_) { + final wc = WindowController.fromWindowId(windowId); + wc.setFullscreen(_fullscreen.isTrue).then((_) { // https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982 if (Platform.isWindows && !v) { Future.delayed(Duration.zero, () async { - final frame = - await WindowController.fromWindowId(windowId).getFrame(); + final frame = await wc.getFrame(); final newRect = Rect.fromLTWH( frame.left, frame.top, frame.width + 1, frame.height + 1); - await WindowController.fromWindowId(windowId).setFrame(newRect); + await wc.setFrame(newRect); }); } }); diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index f36370e40..b8edeb3e4 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -69,8 +69,8 @@ class RustDeskMultiWindowManager { // This function must be called in the main window thread. // Because the _remoteDesktopWindows is managed in that thread. - openMonitorSession( - int windowId, String peerId, int display, int displayCount) async { + openMonitorSession(int windowId, String peerId, int display, int displayCount, + Rect? screenRect) async { if (_remoteDesktopWindows.length > 1) { for (final windowId in _remoteDesktopWindows) { if (await DesktopMultiWindow.invokeMethod( @@ -95,6 +95,14 @@ class RustDeskMultiWindowManager { 'display': display, 'displays': displays, }; + if (screenRect != null) { + params['screen_rect'] = { + 'l': screenRect.left, + 't': screenRect.top, + 'r': screenRect.right, + 'b': screenRect.bottom, + }; + } await _newSession( false, WindowType.RemoteDesktop, @@ -102,21 +110,34 @@ class RustDeskMultiWindowManager { peerId, _remoteDesktopWindows, jsonEncode(params), + screenRect: screenRect, ); } Future newSessionWindow( - WindowType type, String remoteId, String msg, List windows) async { + WindowType type, + String remoteId, + String msg, + List windows, + bool withScreenRect, + ) async { final windowController = await DesktopMultiWindow.createWindow(msg); final windowId = windowController.windowId; - windowController - ..setFrame( - const Offset(0, 0) & Size(1280 + windowId * 20, 720 + windowId * 20)) - ..center() - ..setTitle(getWindowNameWithId( + if (!withScreenRect) { + windowController + ..setFrame(const Offset(0, 0) & + Size(1280 + windowId * 20, 720 + windowId * 20)) + ..center() + ..setTitle(getWindowNameWithId( + remoteId, + overrideType: type, + )); + } else { + windowController.setTitle(getWindowNameWithId( remoteId, overrideType: type, )); + } if (Platform.isMacOS) { Future.microtask(() => windowController.show()); } @@ -131,11 +152,13 @@ class RustDeskMultiWindowManager { String methodName, String remoteId, List windows, - String msg, - ) async { + String msg, { + Rect? screenRect, + }) async { if (openInTabs) { if (windows.isEmpty) { - final windowId = await newSessionWindow(type, remoteId, msg, windows); + final windowId = await newSessionWindow( + type, remoteId, msg, windows, screenRect != null); return MultiWindowCallResult(windowId, null); } else { return call(type, methodName, msg); @@ -144,8 +167,10 @@ class RustDeskMultiWindowManager { if (_inactiveWindows.isNotEmpty) { for (final windowId in windows) { if (_inactiveWindows.contains(windowId)) { - await restoreWindowPosition(type, - windowId: windowId, peerId: remoteId); + if (screenRect == null) { + await restoreWindowPosition(type, + windowId: windowId, peerId: remoteId); + } await DesktopMultiWindow.invokeMethod(windowId, methodName, msg); WindowController.fromWindowId(windowId).show(); registerActiveWindow(windowId); @@ -153,7 +178,8 @@ class RustDeskMultiWindowManager { } } } - final windowId = await newSessionWindow(type, remoteId, msg, windows); + final windowId = await newSessionWindow( + type, remoteId, msg, windows, screenRect != null); return MultiWindowCallResult(windowId, null); } } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 02f3c719e..6a154156b 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -290,6 +290,12 @@ pub struct PeerConfig { skip_serializing_if = "String::is_empty" )] pub displays_as_individual_windows: String, + #[serde( + default = "PeerConfig::default_use_all_my_displays_for_the_remote_session", + deserialize_with = "PeerConfig::deserialize_use_all_my_displays_for_the_remote_session", + skip_serializing_if = "String::is_empty" + )] + pub use_all_my_displays_for_the_remote_session: String, #[serde( default, @@ -335,6 +341,8 @@ impl Default for PeerConfig { view_only: Default::default(), reverse_mouse_wheel: Self::default_reverse_mouse_wheel(), displays_as_individual_windows: Self::default_displays_as_individual_windows(), + use_all_my_displays_for_the_remote_session: + Self::default_use_all_my_displays_for_the_remote_session(), custom_resolutions: Default::default(), options: Self::default_options(), ui_flutter: Default::default(), @@ -561,7 +569,7 @@ impl Config { pub fn get_home() -> PathBuf { #[cfg(any(target_os = "android", target_os = "ios"))] - return Self::path(APP_HOME_DIR.read().unwrap().as_str()); + return PathBuf::from(APP_HOME_DIR.read().unwrap().as_str()); #[cfg(not(any(target_os = "android", target_os = "ios")))] { if let Some(path) = dirs_next::home_dir() { @@ -615,6 +623,13 @@ impl Config { std::fs::create_dir_all(&path).ok(); return path; } + #[cfg(target_os = "android")] + { + let mut path = Self::get_home(); + path.push(format!("{}/Logs", *APP_NAME.read().unwrap())); + std::fs::create_dir_all(&path).ok(); + return path; + } if let Some(path) = Self::path("").parent() { let mut path: PathBuf = path.into(); path.push("log"); @@ -1156,6 +1171,11 @@ impl PeerConfig { deserialize_displays_as_individual_windows, UserDefaultConfig::read().get("displays_as_individual_windows") ); + serde_field_string!( + default_use_all_my_displays_for_the_remote_session, + deserialize_use_all_my_displays_for_the_remote_session, + UserDefaultConfig::read().get("use_all_my_displays_for_the_remote_session") + ); fn default_custom_image_quality() -> Vec { let f: f64 = UserDefaultConfig::read() diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index 4956403a9..d8b176597 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -19,7 +19,7 @@ cfg-if = "1.0" num_cpus = "1.15" lazy_static = "1.4" hbb_common = { path = "../hbb_common" } -webm = "1.0" +webm = { git = "https://github.com/21pages/rust-webm" } [dependencies.winapi] version = "0.3" diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 333f85b98..eaaddbfad 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -362,13 +362,14 @@ pub fn check_config_process() { let f = || { // Clear to avoid checking process errors // But when the program is just started, the configuration file has not been updated, and the new connection will read an empty configuration + // TODO: --server start multi times on windows startup, which will clear the last config and cause concurrent file writing HwCodecConfig::clear(); if let Ok(exe) = std::env::current_exe() { if let Some(_) = exe.file_name().to_owned() { let arg = "--check-hwcodec-config"; if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() { - // wait up to 10 seconds - for _ in 0..10 { + // wait up to 30 seconds, it maybe slow on windows startup for poorly performing machines + for _ in 0..30 { std::thread::sleep(std::time::Duration::from_secs(1)); if let Ok(Some(_)) = child.try_wait() { break; diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index 2893cbf18..3c0ee2a95 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -49,9 +49,12 @@ impl RecorderContext { } let file = if self.server { "s" } else { "c" }.to_string() + &self.id.clone() - + &chrono::Local::now().format("_%Y%m%d%H%M%S_").to_string() - + &self.format.to_string() - + if self.format == CodecFormat::VP9 || self.format == CodecFormat::VP8 { + + &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string() + + &self.format.to_string().to_lowercase() + + if self.format == CodecFormat::VP9 + || self.format == CodecFormat::VP8 + || self.format == CodecFormat::AV1 + { ".webm" } else { ".mp4" @@ -83,6 +86,7 @@ pub enum RecordState { pub struct Recorder { pub inner: Box, ctx: RecorderContext, + pts: Option, } impl Deref for Recorder { @@ -101,19 +105,18 @@ impl DerefMut for Recorder { impl Recorder { pub fn new(mut ctx: RecorderContext) -> ResultType { - if ctx.format == CodecFormat::AV1 { - bail!("not support av1 recording"); - } ctx.set_filename()?; let recorder = match ctx.format { - CodecFormat::VP8 | CodecFormat::VP9 => Recorder { + CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => Recorder { inner: Box::new(WebmRecorder::new(ctx.clone())?), ctx, + pts: None, }, #[cfg(feature = "hwcodec")] _ => Recorder { inner: Box::new(HwRecorder::new(ctx.clone())?), ctx, + pts: None, }, #[cfg(not(feature = "hwcodec"))] _ => bail!("unsupported codec type"), @@ -125,13 +128,16 @@ impl Recorder { fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> { ctx.set_filename()?; self.inner = match ctx.format { - CodecFormat::VP8 | CodecFormat::VP9 => Box::new(WebmRecorder::new(ctx.clone())?), + CodecFormat::VP8 | CodecFormat::VP9 | CodecFormat::AV1 => { + Box::new(WebmRecorder::new(ctx.clone())?) + } #[cfg(feature = "hwcodec")] _ => Box::new(HwRecorder::new(ctx.clone())?), #[cfg(not(feature = "hwcodec"))] _ => bail!("unsupported codec type"), }; self.ctx = ctx; + self.pts = None; self.send_state(RecordState::NewFile(self.ctx.filename.clone())); Ok(()) } @@ -153,7 +159,10 @@ impl Recorder { ..self.ctx.clone() })?; } - vp8s.frames.iter().map(|f| self.write_video(f)).count(); + for f in vp8s.frames.iter() { + self.check_pts(f.pts)?; + self.write_video(f); + } } video_frame::Union::Vp9s(vp9s) => { if self.ctx.format != CodecFormat::VP9 { @@ -162,7 +171,22 @@ impl Recorder { ..self.ctx.clone() })?; } - vp9s.frames.iter().map(|f| self.write_video(f)).count(); + for f in vp9s.frames.iter() { + self.check_pts(f.pts)?; + self.write_video(f); + } + } + video_frame::Union::Av1s(av1s) => { + if self.ctx.format != CodecFormat::AV1 { + self.change(RecorderContext { + format: CodecFormat::AV1, + ..self.ctx.clone() + })?; + } + for f in av1s.frames.iter() { + self.check_pts(f.pts)?; + self.write_video(f); + } } #[cfg(feature = "hwcodec")] video_frame::Union::H264s(h264s) => { @@ -172,8 +196,9 @@ impl Recorder { ..self.ctx.clone() })?; } - if self.ctx.format == CodecFormat::H264 { - h264s.frames.iter().map(|f| self.write_video(f)).count(); + for f in h264s.frames.iter() { + self.check_pts(f.pts)?; + self.write_video(f); } } #[cfg(feature = "hwcodec")] @@ -184,8 +209,9 @@ impl Recorder { ..self.ctx.clone() })?; } - if self.ctx.format == CodecFormat::H265 { - h265s.frames.iter().map(|f| self.write_video(f)).count(); + for f in h265s.frames.iter() { + self.check_pts(f.pts)?; + self.write_video(f); } } _ => bail!("unsupported frame type"), @@ -194,6 +220,17 @@ impl Recorder { Ok(()) } + fn check_pts(&mut self, pts: i64) -> ResultType<()> { + // https://stackoverflow.com/questions/76379101/how-to-create-one-playable-webm-file-from-two-different-video-tracks-with-same-c + let old_pts = self.pts; + self.pts = Some(pts); + if old_pts.clone().unwrap_or_default() > pts { + log::info!("pts {:?}->{}, change record filename", old_pts, pts); + self.change(self.ctx.clone())?; + } + Ok(()) + } + fn send_state(&self, state: RecordState) { self.ctx.tx.as_ref().map(|tx| tx.send(state)); } @@ -230,10 +267,19 @@ impl RecorderApi for WebmRecorder { None, if ctx.format == CodecFormat::VP9 { mux::VideoCodecId::VP9 - } else { + } else if ctx.format == CodecFormat::VP8 { mux::VideoCodecId::VP8 + } else { + mux::VideoCodecId::AV1 }, ); + if ctx.format == CodecFormat::AV1 { + // [129, 8, 12, 0] in 3.6.0, but zero works + let codec_private = vec![0, 0, 0, 0]; + if !webm.set_codec_private(vt.track_number(), &codec_private) { + bail!("Failed to set codec private"); + } + } Ok(WebmRecorder { vt, webm: Some(webm), diff --git a/src/client.rs b/src/client.rs index 445aaf225..5a3c35467 100644 --- a/src/client.rs +++ b/src/client.rs @@ -999,16 +999,19 @@ pub struct VideoHandler { pub rgb: ImageRgb, recorder: Arc>>, record: bool, + _display: usize, // useful for debug } impl VideoHandler { /// Create a new video handler. - pub fn new() -> Self { + pub fn new(_display: usize) -> Self { + log::info!("new video handler for display #{_display}"); VideoHandler { decoder: Decoder::new(), rgb: ImageRgb::new(ImageFormat::ARGB, crate::DST_STRIDE_RGBA), recorder: Default::default(), record: false, + _display, } } @@ -1207,7 +1210,7 @@ impl LoginConfigHandler { self.save_config(config); } - /// Save reverse mouse wheel ("", "Y") to the current config. + /// Save "displays_as_individual_windows" ("", "Y") to the current config. /// /// # Arguments /// @@ -1218,6 +1221,17 @@ impl LoginConfigHandler { self.save_config(config); } + /// Save "use_all_my_displays_for_the_remote_session" ("", "Y") to the current config. + /// + /// # Arguments + /// + /// * `value` - The "use_all_my_displays_for_the_remote_session" value ("", "Y"). + pub fn save_use_all_my_displays_for_the_remote_session(&mut self, value: String) { + let mut config = self.load_config(); + config.use_all_my_displays_for_the_remote_session = value; + self.save_config(config); + } + /// Save scroll style to the current config. /// /// # Arguments @@ -1889,7 +1903,7 @@ where if handler_controller_map.len() <= display { for _i in handler_controller_map.len()..=display { handler_controller_map.push(VideoHandlerController { - handler: VideoHandler::new(), + handler: VideoHandler::new(_i), count: 0, duration: std::time::Duration::ZERO, skip_beginning: 0, @@ -1949,6 +1963,7 @@ where } } MediaData::RecordScreen(start, display, w, h, id) => { + log::info!("record screen command: start:{start}, display:{display}"); if handler_controller_map.len() == 1 { // Compatible with the sciter version(single ui session). // For the sciter version, there're no multi-ui-sessions for one connection. diff --git a/src/common.rs b/src/common.rs index 3b2f5ca3e..cf75fab1b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -108,7 +108,7 @@ impl Drop for SimpleCallOnReturn { pub fn global_init() -> bool { #[cfg(target_os = "linux")] { - if !*IS_X11 { + if !crate::platform::linux::is_x11() { crate::server::wayland::init(); } } @@ -956,7 +956,10 @@ pub async fn post_request_sync(url: String, body: String, header: &str) -> Resul } #[inline] -pub fn make_privacy_mode_msg_with_details(state: back_notification::PrivacyModeState, details: String) -> Message { +pub fn make_privacy_mode_msg_with_details( + state: back_notification::PrivacyModeState, + details: String, +) -> Message { let mut misc = Misc::new(); let mut back_notification = BackNotification { details, @@ -990,17 +993,6 @@ pub fn get_supported_keyboard_modes(version: i64) -> Vec { .collect::>() } -#[cfg(not(target_os = "linux"))] -lazy_static::lazy_static! { - pub static ref IS_X11: bool = false; - -} - -#[cfg(target_os = "linux")] -lazy_static::lazy_static! { - pub static ref IS_X11: bool = hbb_common::platform::linux::is_x11_or_headless(); -} - pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> String { use serde_json::json; let mut fd_json = serde_json::Map::new(); diff --git a/src/core_main.rs b/src/core_main.rs index e7bb4d0f6..f1669d36b 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,4 +1,4 @@ -#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[cfg(windows)] use crate::client::translate; #[cfg(not(debug_assertions))] #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/flutter.rs b/src/flutter.rs index a9cd98149..9d304af8b 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1377,6 +1377,9 @@ pub fn get_cur_session() -> Option { // sessions mod is used to avoid the big lock of sessions' map. pub mod sessions { + #[cfg(feature = "flutter_texture_render")] + use std::collections::HashSet; + use super::*; lazy_static::lazy_static! { @@ -1441,12 +1444,44 @@ pub mod sessions { let mut remove_peer_key = None; for (peer_key, s) in SESSIONS.write().unwrap().iter_mut() { let mut write_lock = s.ui_handler.session_handlers.write().unwrap(); - if write_lock.remove(id).is_some() { + let remove_ret = write_lock.remove(id); + #[cfg(not(feature = "flutter_texture_render"))] + if remove_ret.is_some() { if write_lock.is_empty() { remove_peer_key = Some(peer_key.clone()); } break; } + #[cfg(feature = "flutter_texture_render")] + match remove_ret { + Some(_) => { + if write_lock.is_empty() { + remove_peer_key = Some(peer_key.clone()); + } else { + // Set capture displays if some are not used any more. + let mut remains_displays = HashSet::new(); + for (_, h) in write_lock.iter() { + remains_displays.extend( + h.renderer + .map_display_sessions + .read() + .unwrap() + .keys() + .cloned(), + ); + } + if !remains_displays.is_empty() { + s.capture_displays( + vec![], + vec![], + remains_displays.iter().map(|d| *d as i32).collect(), + ); + } + } + break; + } + None => {} + } } SESSIONS.write().unwrap().remove(&remove_peer_key?) } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ea6ae1180..1484d2440 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -39,11 +39,15 @@ fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); #[cfg(target_os = "android")] { + // flexi_logger can't work when android_logger initialized. + #[cfg(debug_assertions)] android_logger::init_once( android_logger::Config::default() .with_max_level(log::LevelFilter::Debug) // limit log level .with_tag("ffi"), // logs will show under mytag tag ); + #[cfg(not(debug_assertions))] + hbb_common::init_log(false, ""); #[cfg(feature = "mediacodec")] scrap::mediacodec::check_mediacodec(); crate::common::test_rendezvous_server(); @@ -206,6 +210,7 @@ pub fn session_reconnect(session_id: SessionID, force_relay: bool) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.reconnect(force_relay); } + session_on_waiting_for_image_dialog_show(session_id); } pub fn session_toggle_option(session_id: SessionID, value: String) { @@ -339,7 +344,9 @@ pub fn session_set_reverse_mouse_wheel(session_id: SessionID, value: String) { } } -pub fn session_get_displays_as_individual_windows(session_id: SessionID) -> SyncReturn> { +pub fn session_get_displays_as_individual_windows( + session_id: SessionID, +) -> SyncReturn> { if let Some(session) = sessions::get_session_by_session_id(&session_id) { SyncReturn(Some(session.get_displays_as_individual_windows())) } else { @@ -353,6 +360,27 @@ pub fn session_set_displays_as_individual_windows(session_id: SessionID, value: } } +pub fn session_get_use_all_my_displays_for_the_remote_session( + session_id: SessionID, +) -> SyncReturn> { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(Some( + session.get_use_all_my_displays_for_the_remote_session(), + )) + } else { + SyncReturn(None) + } +} + +pub fn session_set_use_all_my_displays_for_the_remote_session( + session_id: SessionID, + value: String, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_use_all_my_displays_for_the_remote_session(value); + } +} + pub fn session_get_custom_image_quality(session_id: SessionID) -> Option> { if let Some(session) = sessions::get_session_by_session_id(&session_id) { Some(session.get_custom_image_quality()) @@ -928,6 +956,25 @@ pub fn main_load_recent_peers_sync() -> SyncReturn { SyncReturn("".to_string()) } +pub fn main_load_lan_peers_sync() -> SyncReturn { + let data = HashMap::from([ + ("name", "load_lan_peers".to_owned()), + ( + "peers", + serde_json::to_string(&get_lan_peers()).unwrap_or_default(), + ), + ]); + return SyncReturn(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); +} + +pub fn main_load_ab_sync() -> SyncReturn { + return SyncReturn(serde_json::to_string(&config::Ab::load()).unwrap_or_default()); +} + +pub fn main_load_group_sync() -> SyncReturn { + return SyncReturn(serde_json::to_string(&config::Group::load()).unwrap_or_default()); +} + pub fn main_load_recent_peers_for_ab(filter: String) -> String { let id_filters = serde_json::from_str::>(&filter).unwrap_or_default(); let id_filters = if id_filters.is_empty() { @@ -1069,6 +1116,29 @@ pub fn main_get_main_display() -> SyncReturn { SyncReturn(display_info) } +pub fn main_get_displays() -> SyncReturn { + #[cfg(target_os = "ios")] + let display_info = "".to_owned(); + #[cfg(not(target_os = "ios"))] + let mut display_info = "".to_owned(); + #[cfg(not(target_os = "ios"))] + if let Ok(displays) = crate::display_service::try_get_displays() { + let displays = displays + .iter() + .map(|d| { + HashMap::from([ + ("x", d.origin().0), + ("y", d.origin().1), + ("w", d.width() as i32), + ("h", d.height() as i32), + ]) + }) + .collect::>(); + display_info = serde_json::to_string(&displays).unwrap_or_default(); + } + SyncReturn(display_info) +} + pub fn session_add_port_forward( session_id: SessionID, local_port: i32, @@ -1515,7 +1585,7 @@ pub fn main_is_installed() -> SyncReturn { pub fn main_start_grab_keyboard() -> SyncReturn { #[cfg(target_os = "linux")] - if !*crate::common::IS_X11 { + if !crate::platform::linux::is_x11() { return SyncReturn(false); } crate::keyboard::client::start_grab_loop(); @@ -1866,6 +1936,17 @@ pub fn is_support_multi_ui_session(version: String) -> SyncReturn { SyncReturn(crate::common::is_support_multi_ui_session(&version)) } +pub fn is_selinux_enforcing() -> SyncReturn { + #[cfg(target_os = "linux")] + { + SyncReturn(crate::platform::linux::is_selinux_enforcing()) + } + #[cfg(not(target_os = "linux"))] + { + SyncReturn(false) + } +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; diff --git a/src/lang/ar.rs b/src/lang/ar.rs index e30621c21..a63e786d1 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e70c2c044..ff43d0ef4 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index ba2796157..c544d225c 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", "切换到主显示器,因为提权后,不支持多显示器画面。"), ("Open in new window", "在新的窗口中打开"), ("Show displays as individual windows", "在单个窗口中打开显示器"), + ("Use all my displays for the remote session", "将我的所有显示器用于远程会话"), + ("selinux_tip", "SELinux 处于启用状态,RustDesk 可能无法作为被控正常运行。"), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 8907e78e0..3ac90b7b5 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -555,14 +555,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Check for software update on startup", "Kontrola aktualizace softwaru při spuštění"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Aktualizujte prosím RustDesk Server Pro na verzi {} nebo novější!"), ("pull_group_failed_tip", "Nepodařilo se obnovit skupinu"), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("switch_display_elevated_connections_tip", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("elevated_switch_display_msg", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), + ("Filter by intersection", "Filtrovat podle průsečíku"), + ("Remove wallpaper during incoming sessions", "Odstranit tapetu během příchozích relací"), + ("Test", "Test"), + ("switch_display_elevated_connections_tip", "Přepnutí na jinou než primární obrazovku není podporováno ve zvýšeném režimu, pokud existuje více připojení. Pokud chcete ovládat více obrazovek, zkuste to po instalaci znovu."), + ("display_is_plugged_out_msg", "Obrazovka je odpojena, přepněte na první obrazovku."), + ("No displays", "Žádné obrazovky"), + ("elevated_switch_display_msg", "Přepnout na primární obrazovku, protože více obrazovek není podporováno ve zvýšeném režimu."), + ("Open in new window", "Otevřít v novém okně"), + ("Show displays as individual windows", "Zobrazit obrazovky jako jednotlivá okna"), + ("Use all my displays for the remote session", "Použít všechny mé obrazovky pro vzdálenou relaci"), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 9d76b8e2e..40f1fbfb0 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index b67ef32b3..9696b62b2 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -440,7 +440,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Stark"), ("Switch Sides", "Seiten wechseln"), ("Please confirm if you want to share your desktop?", "Bitte bestätigen Sie, wenn Sie Ihren Desktop freigeben möchten."), - ("Display", "Anzeige"), + ("Display", "Bildschirm"), ("Default View Style", "Standard-Ansichtsstil"), ("Default Scroll Style", "Standard-Scroll-Stil"), ("Default Image Quality", "Standard-Bildqualität"), @@ -476,7 +476,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Password", "Leeres Passwort"), ("Me", "Ich"), ("identical_file_tip", "Diese Datei ist identisch mit der Datei der Gegenstelle."), - ("show_monitors_tip", "Monitore in der Symbolleiste anzeigen"), + ("show_monitors_tip", "Bildschirme in der Symbolleiste anzeigen"), ("View Mode", "Ansichtsmodus"), ("login_linux_tip", "Sie müssen sich an einem entfernten Linux-Konto anmelden, um eine X-Desktop-Sitzung zu eröffnen."), ("verify_rustdesk_password_tip", "RustDesk-Passwort bestätigen"), @@ -558,11 +558,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", "Nach Schnittmenge filtern"), ("Remove wallpaper during incoming sessions", "Hintergrundbild während eingehender Sitzungen entfernen"), ("Test", "Test"), - ("switch_display_elevated_connections_tip", "Das Umschalten auf eine nicht primäre Anzeige wird mit erhöhten Rechten nicht unterstützt, wenn mehrere Verbindungen bestehen. Bitte versuchen Sie es nach der Installation erneut, wenn Sie mehrere Anzeigen steuern möchten."), - ("display_is_plugged_out_msg", "Das Anzeigegerät ist nicht angeschlossen, schalten Sie auf das erste Anzeigegerät um."), - ("No displays", "Keine Anzeigegeräte"), - ("elevated_switch_display_msg", "Wechseln Sie zur primären Anzeige, da die Mehrfachanzeige im erweiterten Modus nicht unterstützt wird."), + ("switch_display_elevated_connections_tip", "Das Umschalten auf einen sekundären Bildschirm wird mit erhöhten Rechten nicht unterstützt, wenn mehrere Verbindungen bestehen. Bitte versuchen Sie es nach der Installation erneut, wenn Sie mehrere Bildschirme steuern möchten."), + ("display_is_plugged_out_msg", "Der Bildschirm ist nicht angeschlossen, schalten Sie auf den ersten Bildschirm um."), + ("No displays", "Keine Bildschirme"), + ("elevated_switch_display_msg", "Wechseln Sie zum primären Bildschirm, da mehrere Bildschirme im erweiterten Modus nicht unterstützt werden."), ("Open in new window", "In einem neuen Fenster öffnen"), - ("Show displays as individual windows", "Anzeigen als einzelne Fenster darstellen"), + ("Show displays as individual windows", "Jeden Bildschirm in einem eigenen Fenster anzeigen"), + ("Use all my displays for the remote session", "Alle meine Bildschirme für die Fernsitzung verwenden"), + ("selinux_tip", "SELinux ist auf Ihrem Gerät aktiviert, was dazu führen kann, dass RustDesk als kontrollierte Seite nicht richtig läuft."), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 472b494e7..72a4c492c 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index e2495780e..209772ccc 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -223,7 +223,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("pull_group_failed_tip", "Failed to refresh group"), ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), ("switch_display_elevated_connections_tip", "Switching to non-primary display is not supported in the elevated mode when there are multiple connections. Please try again after installation if you want to control multiple displays."), - ("display_is_plugged_out_msg", "The diplay is plugged out, switch to the first display."), - ("elevated_switch_display_msg", "Switch to the primary display because multiple display is not supported in elevated mode."), + ("display_is_plugged_out_msg", "The display is plugged out, switch to the first display."), + ("elevated_switch_display_msg", "Switch to the primary display because multiple displays are not supported in elevated mode."), + ("selinux_tip", "SELinux is enabled on your device, which may prevent RustDesk from running properly as controlled side."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index ccc674749..1d91e7b7f 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 1b1b83afa..26bb408ac 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -555,14 +555,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Check for software update on startup", "Comprobar actualización al iniciar"), ("upgrade_rustdesk_server_pro_to_{}_tip", "¡Por favor, actualiza RustDesk Server Pro a la versión {} o superior"), ("pull_group_failed_tip", "No se ha podido refrescar el grupo"), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("switch_display_elevated_connections_tip", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("elevated_switch_display_msg", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), + ("Filter by intersection", "Filtrar por intersección"), + ("Remove wallpaper during incoming sessions", "Quitar el fonde de pantalla durante sesiones entrantes"), + ("Test", "Probar"), + ("switch_display_elevated_connections_tip", "Cambiar a una pantalla no principal no está soportado en el modo elevado cuando hay múltiples conexiones. Por favor, inténtalo de nuevo tras la instalación si quieres controlar múltiples pantallas."), + ("display_is_plugged_out_msg", "La pantalla está desconectada, cambia a la principal."), + ("No displays", "No hay pantallas"), + ("elevated_switch_display_msg", "Cambiar a la pantalla principal porque mútliples pantallas no están soportadas en modo elevado."), + ("Open in new window", "Abrir en una nueva ventana"), + ("Show displays as individual windows", "Mostrar pantallas como ventanas individuales"), + ("Use all my displays for the remote session", "Usar todas mis pantallas para la sesión remota"), + ("selinux_tip", "SELinux está activado en tu dispositivo, lo que puede hacer que RustDesk no se ejecute correctamente como lado controlado."), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index ff90b0430..69f883b89 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 2195d4fdd..e15bc38ba 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 24ac09d05..f18013423 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 2f37c3f07..ce9e359b1 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -52,7 +52,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Home", ""), ("Audio Input", "Input Audio"), ("Enhancements", "Peningkatan"), - ("Hardware Codec", "Codec Perangkat Keras"), + ("Hardware Codec", "Kodek Perangkat Keras"), ("Adaptive bitrate", "Kecepatan Bitrate Adaptif"), ("ID Server", "Server ID"), ("Relay Server", "Server Relay"), @@ -352,7 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark", "Gelap"), ("Light", "Terang"), ("Follow System", "Ikuti Sistem"), - ("Enable hardware codec", "Aktifkan codec perangkat keras"), + ("Enable hardware codec", "Aktifkan kodek perangkat keras"), ("Unlock Security Settings", "Buka Keamanan Pengaturan"), ("Enable Audio", "Aktifkan Audio"), ("Unlock Network Settings", "Buka Keamanan Pengaturan Jaringan"), @@ -369,7 +369,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin Toolbar", "Batal sematkan Toolbar"), ("Recording", "Perekaman"), ("Directory", "Direktori"), - ("Automatically record incoming sessions", "Secara otomatis merekam sesi masuk"), + ("Automatically record incoming sessions", "Otomatis merekam sesi masuk"), ("Change", "Ubah"), ("Start session recording", "Mulai sesi perekaman"), ("Stop session recording", "Hentikan sesi perekaman"), @@ -444,7 +444,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Default View Style", "Gaya Tampilan Default"), ("Default Scroll Style", "Gaya Scroll Default"), ("Default Image Quality", "Kualitas Gambar Default"), - ("Default Codec", "Codec default"), + ("Default Codec", "Kodek default"), ("Bitrate", "Bitrate"), ("FPS", "FPS"), ("Auto", "Otomatis"), @@ -454,9 +454,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Hentikan panggilan suara"), ("relay_hint_tip", "Tidak memungkinkan untuk terhubung secara langsung; anda bisa mencoba terhubung via relay. Selain itu, jika ingin menggunakan relay pada percobaan pertama, silahkan tambah akhiran \"/r\" pada ID atau pilih \"Selalu terhubung via relay\" di pilihan sesi terbaru."), ("Reconnect", "Menyambungkan ulang"), - ("Codec", "Codec"), + ("Codec", "Kodek"), ("Resolution", "Resolusi"), - ("No transfers in progress", "Tidak ada transfer data yang sedang berlangsung"), + ("No transfers in progress", "Tidak ada proses transfer data"), ("Set one-time password length", "Atur panjang kata sandi sekali pakai"), ("install_cert_tip", "Install sertifikat RustDesk"), ("confirm_install_cert_tip", "Ini adalah sertifikat pengujian RustDesk, yang dapat dipercaya. Sertifikat ini akan digunakan untuk menginstal driver RustDesk saat diperlukan"), @@ -541,7 +541,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("HSV Color", "Warna HSV"), ("Installation Successful!", "Instalasi berhasil!"), ("Installation failed!", "Instalasi gagal!"), - ("Reverse mouse wheel", "Balikkan arah scroll mouse!"), + ("Reverse mouse wheel", "Balikkan arah scroll mouse"), ("{} sessions", "sesi {}"), ("scam_title", "Kemungkinan Anda Sedang DITIPU!"), ("scam_text1", "Jika Anda sedang berbicara di telepon dengan seseorang yang TIDAK dikenal dan mereka meminta anda untuk menggunakan RustDesk, jangan lanjutkan dan segera tutup panggilan."), @@ -563,6 +563,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", "Tidak ada tampilan"), ("elevated_switch_display_msg", "Pindah ke tampilan utama, pada mode elevasi, pengggunaan lebih dari satu layar tidak diizinkan"), ("Open in new window", "Buka di jendela baru"), - ("Show displays as individual windows", "Tampilkan layar sebagai jendela terpisah"), + ("Show displays as individual windows", "Tampilkan dengan jendela terpisah"), + ("Use all my displays for the remote session", "Gunakan semua layar untuk sesi remote"), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index cb7ed922c..0c0989764 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -365,7 +365,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Dispositivo ingresso audio"), ("Use IP Whitelisting", "Usa elenco IP autorizzati"), ("Network", "Rete"), - ("Enable RDP", "Abilita RDP"), ("Pin Toolbar", "Blocca barra strumenti"), ("Unpin Toolbar", "Sblocca barra strumenti"), ("Recording", "Registrazione"), @@ -565,5 +564,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", "Passo allo schermo principale perché in modalità elevata non sono supportati più schermi."), ("Open in new window", "Apri in una nuova finestra"), ("Show displays as individual windows", "Visualizza schermi come finestre individuali"), + ("Use all my displays for the remote session", "Usa tutti gli schermi per la sessione remota"), + ("selinux_tip", ""), + ("selinux_tip", "In questo dispositivo è abilitato SELinux, che potrebbe impedire il corretto funzionamento di RustDesk come lato controllato."), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index eeed3e147..9c60683f3 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index dcf28718a..189fa640a 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 43c36e32c..b6475c5f9 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 02d4714c2..ebeebaaa0 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 1af747f23..033b45d0d 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", "Pārslēdzieties uz primāro displeju, jo paaugstinātajā režīmā netiek atbalstīti vairāki displeji."), ("Open in new window", "Atvērt jaunā logā"), ("Show displays as individual windows", "Rādīt displejus kā atsevišķus logus"), + ("Use all my displays for the remote session", "Izmantot visus manus displejus attālajai sesijai"), + ("selinux_tip", "Jūsu ierīcē ir iespējots SELinux, kas var neļaut RustDesk pareizi darboties kā kontrolētajai pusei."), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 71d1c1107..9d8445825 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -555,14 +555,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Check for software update on startup", "Checken voor updates bij opstarten"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Upgrade RustDesk Server Pro naar versie {} of nieuwer!"), ("pull_group_failed_tip", "Vernieuwen van groep mislukt"), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("switch_display_elevated_connections_tip", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("elevated_switch_display_msg", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), + ("Filter by intersection", "Filter op kruising"), + ("Remove wallpaper during incoming sessions", "Achtergrond verwijderen tijdens inkomende sessies"), + ("Test", "Test"), + ("switch_display_elevated_connections_tip", "Overschakelen naar een niet-hoofdbeeldscherm wordt niet ondersteund in de verhoogde modus wanneer er meerdere verbindingen zijn. Probeer het opnieuw na de installatie als je meerdere schermen wilt beheren."), + ("display_is_plugged_out_msg", "Beeldscherm is uitgeschakeld, schakel over naar het primaire beeldscherm."), + ("No displays", "Geen beeldschermen"), + ("elevated_switch_display_msg", "Schakel over naar het primaire beeldscherm, aangezien meerdere beeldschermen niet worden ondersteund in de modus met verhoogde rechten."), + ("Open in new window", "Open in een nieuw venster"), + ("Show displays as individual windows", "Beeldschermen weergeven als afzonderlijke vensters"), + ("Use all my displays for the remote session", "Gebruik al mijn beeldschermen voor de externe sessie"), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 88dc5cda5..0c474aea0 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", "Przełącz się na ekran główny, ponieważ wyświetlanie kilku ekranów nie jest obsługiwane przy podniesionych uprawnieniach."), ("Open in new window", "Otwórz w nowym oknie"), ("Show displays as individual windows", "Pokaż ekrany w osobnych oknach"), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 85863d2ae..77c451035 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index be4cdfd8e..4fedb1636 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 311a13bec..700cf452f 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 9d3872871..3bb2e9820 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", "Переключитесь на основной дисплей, поскольку в режиме повышенных прав несколько дисплеев не поддерживаются."), ("Open in new window", "Открыть в новом окне"), ("Show displays as individual windows", "Показывать дисплеи в отдельных окнах"), + ("Use all my displays for the remote session", "Использовать все мои дисплеи для удалённого сеанса"), + ("selinux_tip", "На вашем устройстве включён SELinux, что может помешать правильной работе RustDesk на контролируемой стороне."), + ("Change view", "Вид"), + ("Big tiles", "Большие значки"), + ("Small tiles", "Маленькие значки"), + ("List", "Список"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 23751b734..c6f6d5724 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 8f84b181a..4e0c394d6 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 3d26b5635..2b37c35b2 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 03f4776b7..63d0190ef 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index ce2dab684..c8b2f22a6 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 04d3d53ef..b358f3709 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 75afe0324..606993498 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index e51cff3db..ecab140e3 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index c70cf8f35..d5a233f02 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 94e0bc423..f8bf4c278 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -138,7 +138,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please try later", "Будь ласка, спробуйте пізніше"), ("Remote desktop is offline", "Віддалена стільниця не в мережі"), ("Key mismatch", "Невідповідність ключів"), - ("Timeout", "Тайм-аут"), + ("Timeout", "Час очікування"), ("Failed to connect to relay server", "Не вдалося підключитися до сервера реле"), ("Failed to connect via rendezvous server", "Не вдалося підключитися через проміжний сервер"), ("Failed to connect via relay server", "Не вдалося підключитися через сервер реле"), @@ -223,7 +223,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Verification code", "Код підтвердження"), ("verification_tip", "Виявлено новий пристрій, код підтвердження надіслано на зареєстровану email-адресу, введіть код підтвердження для продовження авторизації."), ("Logout", "Вийти"), - ("Tags", "Ключові слова"), + ("Tags", "Теги"), ("Search ID", "Пошук за ID"), ("whitelist_sep", "Розділені комою, крапкою з комою, пробілом або новим рядком"), ("Add ID", "Додати ID"), @@ -231,7 +231,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unselect all tags", "Скасувати вибір усіх тегів"), ("Network error", "Помилка мережі"), ("Username missed", "Імʼя користувача відсутнє"), - ("Password missed", "Забули пароль"), + ("Password missed", "Пароль відсутній"), ("Wrong credentials", "Неправильні дані"), ("The verification code is incorrect or has expired", "Код підтвердження некоректний або протермінований"), ("Edit Tag", "Редагувати тег"), @@ -269,7 +269,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reset canvas", "Відновлення полотна"), ("No permission of file transfer", "Немає дозволу на передачу файлів"), ("Note", "Примітка"), - ("Connection", "Зʼєднання"), + ("Connection", "Підключення"), ("Share Screen", "Поділитися екраном"), ("Chat", "Чат"), ("Total", "Всього"), @@ -516,53 +516,59 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Exit", "Вийти"), ("Open", "Відкрити"), ("logout_tip", "Ви впевнені, що хочете вилогуватися?"), - ("Service", ""), - ("Start", ""), - ("Stop", ""), - ("exceed_max_devices", ""), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), - ("Already exists", ""), - ("Change Password", ""), - ("Refresh Password", ""), - ("ID", ""), - ("Grid View", ""), - ("List View", ""), - ("Select", ""), - ("Toggle Tags", ""), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), - ("Change Color", ""), - ("Primary Color", ""), - ("HSV Color", ""), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("switch_display_elevated_connections_tip", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("elevated_switch_display_msg", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), + ("Service", "Служба"), + ("Start", "Запустити"), + ("Stop", "Зупинити"), + ("exceed_max_devices", "У вас максимальна кількість керованих пристроїв."), + ("Sync with recent sessions", "Синхронізація з нещодавніми сеансами"), + ("Sort tags", "Сортувати теги"), + ("Open connection in new tab", "Відкрити підключення в новій вкладці"), + ("Move tab to new window", "Перемістити вкладку до нового вікна"), + ("Can not be empty", "Не може бути порожнім"), + ("Already exists", "Вже існує"), + ("Change Password", "Змінити пароль"), + ("Refresh Password", "Оновити пароль"), + ("ID", "ID"), + ("Grid View", "Перегляд ґраткою"), + ("List View", "Перегляд списком"), + ("Select", "Вибрати"), + ("Toggle Tags", "Видимість тегів"), + ("pull_ab_failed_tip", "Не вдалося оновити адресну книгу"), + ("push_ab_failed_tip", "Не вдалося синхронізувати адресну книгу"), + ("synced_peer_readded_tip", "Пристрої з нещодавніх сеансів будуть синхронізовані з адресною книгою"), + ("Change Color", "Змінити колір"), + ("Primary Color", "Основний колір"), + ("HSV Color", "Колір HSV"), + ("Installation Successful!", "Успішне встановлення!"), + ("Installation failed!", "Невдале встановлення!"), + ("Reverse mouse wheel", "Зворотній напрям прокрутки"), + ("{} sessions", "{} сеансів"), + ("scam_title", "Вас можуть ОБМАНУТИ!"), + ("scam_text1", "Якщо ви розмовляєте по телефону з кимось, кого НЕ ЗНАЄТЕ чи кому НЕ ДОВІРЯЄТЕ, і ця особа хоче, щоб ви використали RustDesk та запустили службу, не робіть цього та негайно завершіть дзвінок."), + ("scam_text2", "Ймовірно, ви маєте справу з шахраєм, що намагається викрасти ваші гроші чи особисті дані."), + ("Don't show again", "Не показувати знову"), + ("I Agree", "Я погоджуюсь"), + ("Decline", "Я не погоджуюсь"), + ("Timeout in minutes", "Час очікування в хвилинах"), + ("auto_disconnect_option_tip", "Автоматично завершувати вхідні сеанси в разі неактивності користувача"), + ("Connection failed due to inactivity", "Автоматично відключено через неактивність"), + ("Check for software update on startup", "Перевіряти оновлення під час запуску"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Будь ласка, оновіть RustDesk Server Pro до версії {} чи новіше!"), + ("pull_group_failed_tip", "Не вдалося оновити групу"), + ("Filter by intersection", "Фільтр за збігом"), + ("Remove wallpaper during incoming sessions", "Прибирати шпалеру під час вхідних сеансів"), + ("Test", "Тест"), + ("switch_display_elevated_connections_tip", "В режимі розширених прав, коли є декілька підключень, не підтримується перемикання на неосновний дисплей. Якщо вам потрібен контроль декількох дисплеїв, будь ласка, спробуйте ще раз після встановлення"), + ("display_is_plugged_out_msg", "Дисплей відключено, перемкніться на перший дисплей"), + ("No displays", "Відсутні дисплеї"), + ("elevated_switch_display_msg", "Перемкніться на основний дисплей, оскільки в режимі розширених прав одночасне використання декілька дисплеїв не підтримуються."), + ("Open in new window", "Відкрити в новому вікні"), + ("Show displays as individual windows", "Відображати дисплеї в якості окремих вікон"), + ("Use all my displays for the remote session", "Використовувати всі мої дисплеї для віддаленого сеансу"), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 895e7491a..103ef2663 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -564,5 +564,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_switch_display_msg", ""), ("Open in new window", ""), ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), ].iter().cloned().collect(); } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 37b27cf64..0d97b317a 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -35,6 +35,10 @@ type Xdo = *const c_void; pub const PA_SAMPLE_RATE: u32 = 48000; static mut UNMODIFIED: bool = true; +lazy_static::lazy_static! { + pub static ref IS_X11: bool = hbb_common::platform::linux::is_x11_or_headless(); +} + thread_local! { static XDO: RefCell = RefCell::new(unsafe { xdo_new(std::ptr::null()) }); static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())}); @@ -1360,3 +1364,26 @@ impl Drop for WallPaperRemover { } } } + +#[inline] +pub fn is_x11() -> bool { + *IS_X11 +} + +#[inline] +pub fn is_selinux_enforcing() -> bool { + match run_cmds("getenforce") { + Ok(output) => output.trim() == "Enforcing", + Err(_) => match run_cmds("sestatus") { + Ok(output) => { + for line in output.lines() { + if line.contains("Current mode:") { + return line.contains("enforcing"); + } + } + false + } + Err(_) => false, + }, + } +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index f664b1aee..3b44cbc11 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1839,20 +1839,19 @@ pub fn uninstall_cert() -> ResultType<()> { mod cert { use hbb_common::{allow_err, bail, log, ResultType}; - use std::{path::Path, str::from_utf8}; + use std::{ffi::OsStr, io::Error, os::windows::ffi::OsStrExt, path::Path, str::from_utf8}; use winapi::{ shared::{ minwindef::{BYTE, DWORD, FALSE, TRUE}, ntdef::NULL, }, um::{ - errhandlingapi::GetLastError, wincrypt::{ CertAddEncodedCertificateToStore, CertCloseStore, CertDeleteCertificateFromStore, - CertEnumCertificatesInStore, CertNameToStrA, CertOpenSystemStoreW, - CryptHashCertificate, ALG_ID, CALG_SHA1, CERT_ID_SHA1_HASH, - CERT_STORE_ADD_REPLACE_EXISTING, CERT_X500_NAME_STR, PCCERT_CONTEXT, - X509_ASN_ENCODING, + CertEnumCertificatesInStore, CertNameToStrA, CertOpenStore, CryptHashCertificate, + ALG_ID, CALG_SHA1, CERT_ID_SHA1_HASH, CERT_STORE_ADD_REPLACE_EXISTING, + CERT_STORE_PROV_SYSTEM_W, CERT_SYSTEM_STORE_LOCAL_MACHINE, CERT_X500_NAME_STR, + PCCERT_CONTEXT, PKCS_7_ASN_ENCODING, X509_ASN_ENCODING, }, winreg::HKEY_LOCAL_MACHINE, }, @@ -1866,8 +1865,12 @@ mod cert { "SOFTWARE\\Microsoft\\SystemCertificates\\ROOT\\Certificates\\"; const THUMBPRINT_ALG: ALG_ID = CALG_SHA1; const THUMBPRINT_LEN: DWORD = 20; - const CERT_ISSUER_1: &str = "CN=\"WDKTestCert admin,133225435702113567\"\0"; + const CERT_ENCODING_TYPE: DWORD = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING; + + lazy_static::lazy_static! { + static ref CERT_STORE_LOC: Vec = OsStr::new("ROOT\0").encode_wide().collect::>(); + } #[inline] unsafe fn compute_thumbprint(pb_encoded: *const BYTE, cb_encoded: DWORD) -> (Vec, String) { @@ -1946,24 +1949,46 @@ mod cert { fn install_cert_add_cert_store(cert_bytes: &mut [u8]) -> ResultType<()> { unsafe { - let store_handle = CertOpenSystemStoreW(0 as _, "ROOT\0".as_ptr() as _); + let store_handle = CertOpenStore( + CERT_STORE_PROV_SYSTEM_W, + 0, + 0, + CERT_SYSTEM_STORE_LOCAL_MACHINE, + CERT_STORE_LOC.as_ptr() as _, + ); if store_handle.is_null() { - bail!("Error opening certificate store: {}", GetLastError()); + bail!( + "Error opening certificate store: {}", + Error::last_os_error() + ); } - let mut cert_ctx: PCCERT_CONTEXT = std::ptr::null_mut(); + + // Create the certificate context + let cert_context = winapi::um::wincrypt::CertCreateCertificateContext( + CERT_ENCODING_TYPE, + cert_bytes.as_ptr(), + cert_bytes.len() as DWORD, + ); + if cert_context.is_null() { + bail!( + "Error creating certificate context: {}", + Error::last_os_error() + ); + } + if FALSE == CertAddEncodedCertificateToStore( store_handle, - X509_ASN_ENCODING, - cert_bytes.as_mut_ptr(), - cert_bytes.len() as _, + CERT_ENCODING_TYPE, + (*cert_context).pbCertEncoded, + (*cert_context).cbCertEncoded, CERT_STORE_ADD_REPLACE_EXISTING, - &mut cert_ctx as _, + std::ptr::null_mut(), ) { log::error!( "Failed to call CertAddEncodedCertificateToStore: {}", - GetLastError() + Error::last_os_error() ); } else { log::info!("Add cert to store successfully"); @@ -1981,12 +2006,20 @@ mod cert { let mut buf = [0u8; 1024]; unsafe { - let store_handle = CertOpenSystemStoreW(0 as _, "ROOT\0".as_ptr() as _); + let store_handle = CertOpenStore( + CERT_STORE_PROV_SYSTEM_W, + 0, + 0, + CERT_SYSTEM_STORE_LOCAL_MACHINE, + CERT_STORE_LOC.as_ptr() as _, + ); if store_handle.is_null() { - bail!("Error opening certificate store: {}", GetLastError()); + bail!( + "Error opening certificate store: {}", + Error::last_os_error() + ); } - let mut vec_ctx = Vec::new(); let mut cert_ctx: PCCERT_CONTEXT = CertEnumCertificatesInStore(store_handle, NULL as _); while !cert_ctx.is_null() { // https://stackoverflow.com/a/66432736 @@ -1998,11 +2031,9 @@ mod cert { buf.len() as _, ); if cb_size != 1 { - let mut add_ctx = false; if let Ok(issuer) = from_utf8(&buf[..cb_size as _]) { for iss in issuers_to_rm.iter() { if issuer == *iss { - add_ctx = true; let (_, thumbprint) = compute_thumbprint( (*cert_ctx).pbCertEncoded, (*cert_ctx).cbCertEncoded, @@ -2010,18 +2041,15 @@ mod cert { if !thumbprint.is_empty() { thumbprints.push(thumbprint); } + // Delete current cert context and re-enumerate. + CertDeleteCertificateFromStore(cert_ctx); + cert_ctx = CertEnumCertificatesInStore(store_handle, NULL as _); } } } - if add_ctx { - vec_ctx.push(cert_ctx); - } } cert_ctx = CertEnumCertificatesInStore(store_handle, cert_ctx); } - for ctx in vec_ctx { - CertDeleteCertificateFromStore(ctx); - } CertCloseStore(store_handle, 0); } @@ -2033,7 +2061,8 @@ mod cert { let reg_cert_key = unsafe { open_reg_cert_store()? }; log::info!("Found {} certs to remove", thumbprints.len()); for thumbprint in thumbprints.iter() { - allow_err!(reg_cert_key.delete_subkey(thumbprint)); + // Deleting cert from registry may fail, because the CertDeleteCertificateFromStore() is called before. + let _ = reg_cert_key.delete_subkey(thumbprint); } Ok(()) } diff --git a/src/server/connection.rs b/src/server/connection.rs index 39dae9868..cc8e02e98 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -611,7 +611,7 @@ impl Connection { _ => {}, } } - Some(message::Union::PeerInfo(_)) => { + Some(message::Union::PeerInfo(..)) => { conn.refresh_video_display(None); } _ => {} @@ -1132,11 +1132,13 @@ impl Connection { self.send(msg_out).await; } - match super::display_service::get_displays().await { + match super::display_service::update_get_sync_displays().await { Err(err) => { res.set_error(format!("{}", err)); } Ok(displays) => { + // For compatibility with old versions, we need to send the displays to the peer. + // But the displays may be updated later, before creating the video capturer. pi.displays = displays.clone(); pi.current_display = self.display_idx as _; res.set_peer_info(pi); @@ -2102,7 +2104,7 @@ impl Connection { display, video_service::OPTION_REFRESH, super::service::SERVICE_OPTION_VALUE_TRUE, - ) + ); }); } @@ -2139,13 +2141,13 @@ impl Connection { ..Default::default() }); } + } - // send display changed message - if let Some(msg_out) = - video_service::make_display_changed_msg(self.display_idx, None) - { - self.send(msg_out).await; - } + // Send display changed message. + // For compatibility with old versions ( < 1.2.4 ). + // sciter need it in new version + if let Some(msg_out) = video_service::make_display_changed_msg(self.display_idx, None) { + self.send(msg_out).await; } } } diff --git a/src/server/display_service.rs b/src/server/display_service.rs index dd4410312..62f5cac3d 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -1,4 +1,6 @@ use super::*; +#[cfg(target_os = "linux")] +use crate::platform::linux::is_x11; #[cfg(all(windows, feature = "virtual_display_driver"))] use crate::virtual_display_manager; #[cfg(windows)] @@ -6,6 +8,8 @@ use hbb_common::get_version_number; use hbb_common::protobuf::MessageField; use scrap::Display; +// https://github.com/rustdesk/rustdesk/discussions/6042, avoiding dbus call + pub const NAME: &'static str = "display"; struct ChangedResolution { @@ -19,6 +23,71 @@ lazy_static::lazy_static! { // Initial primary display index. // It should only be updated when the rustdesk server is started, and should not be updated when displays changed. pub static ref PRIMARY_DISPLAY_IDX: usize = get_primary(); + static ref SYNC_DISPLAYS: Arc> = Default::default(); +} + +#[derive(Default)] +struct SyncDisplaysInfo { + displays: Vec, + is_synced: bool, +} + +impl SyncDisplaysInfo { + fn check_changed(&mut self, displays: Vec) { + if self.displays.len() != displays.len() { + self.displays = displays; + self.is_synced = false; + return; + } + for (i, d) in displays.iter().enumerate() { + if d != &self.displays[i] { + self.displays = displays; + self.is_synced = false; + return; + } + } + } + + fn get_update_sync_displays(&mut self) -> Option> { + if self.is_synced { + return None; + } + self.is_synced = true; + Some(self.displays.clone()) + } +} + +// This function is really useful, though a duplicate check if display changed. +// The video server will then send the following messages to the client: +// 1. the supported resolutions of the {idx} display +// 2. the switch resolution message, so that the client can record the custom resolution. +pub(super) fn check_display_changed( + ndisplay: usize, + idx: usize, + (x, y, w, h): (i32, i32, usize, usize), +) -> Option { + #[cfg(target_os = "linux")] + { + // wayland do not support changing display for now + if !is_x11() { + return None; + } + } + + let lock = SYNC_DISPLAYS.lock().unwrap(); + // If plugging out a monitor && lock.displays.get(idx) is None. + // 1. The client version < 1.2.4. The client side has to reconnect. + // 2. The client version > 1.2.4, The client side can handle the case becuase sync peer info message will be sent. + // But it is acceptable to for the user to reconnect manually, becuase the monitor is unplugged. + let d = lock.displays.get(idx)?; + if ndisplay != lock.displays.len() { + return Some(d.clone()); + } + if !(d.x == x && d.y == y && d.width == w as i32 && d.height == h as i32) { + Some(d.clone()) + } else { + None + } } #[inline] @@ -74,45 +143,29 @@ pub fn is_privacy_mode_supported() -> bool { return false; } -#[derive(Default)] -struct StateDisplay { - synced_displays: Vec, -} - -impl super::service::Reset for StateDisplay { - fn reset(&mut self) { - self.synced_displays.clear(); - } -} - pub fn new() -> GenericService { - let svc = EmptyExtraFieldService::new(NAME.to_owned(), false); - GenericService::repeat::(&svc.clone(), 300, run); + let svc = EmptyExtraFieldService::new(NAME.to_owned(), true); + GenericService::run(&svc.clone(), run); svc.sp } -fn check_get_displays_changed_msg(last_synced_displays: &mut Vec) -> Option { - let displays = try_get_displays().ok()?; - if displays.len() == last_synced_displays.len() { - return None; - } +fn displays_to_msg(displays: Vec) -> Message { + let mut pi = PeerInfo { + ..Default::default() + }; + pi.displays = displays.clone(); + // current_display should not be used in server. + // It is set to 0 for compatibility with old clients. + pi.current_display = 0; + let mut msg_out = Message::new(); + msg_out.set_peer_info(pi); + msg_out +} - // Display to DisplayInfo - let displays = to_display_info(&displays); - if last_synced_displays.len() == 0 { - *last_synced_displays = displays; - None - } else { - let mut pi = PeerInfo { - ..Default::default() - }; - pi.displays = displays.clone(); - pi.current_display = 0; - let mut msg_out = Message::new(); - msg_out.set_peer_info(pi); - *last_synced_displays = displays; - Some(msg_out) - } +fn check_get_displays_changed_msg() -> Option { + check_update_displays(&try_get_displays().ok()?); + let displays = SYNC_DISPLAYS.lock().unwrap().get_update_sync_displays()?; + Some(displays_to_msg(displays)) } #[cfg(all(windows, feature = "virtual_display_driver"))] @@ -120,11 +173,23 @@ pub fn try_plug_out_virtual_display() { let _res = virtual_display_manager::plug_out_headless(); } -fn run(sp: EmptyExtraFieldService, state: &mut StateDisplay) -> ResultType<()> { - if let Some(msg_out) = check_get_displays_changed_msg(&mut state.synced_displays) { - sp.send(msg_out); - log::info!("Displays changed"); +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + while sp.ok() { + sp.snapshot(|sps| { + if sps.has_subscribes() { + SYNC_DISPLAYS.lock().unwrap().is_synced = false; + bail!("new subscriber"); + } + Ok(()) + })?; + + if let Some(msg_out) = check_get_displays_changed_msg() { + sp.send(msg_out); + log::info!("Displays changed"); + } + std::thread::sleep(Duration::from_millis(300)); } + Ok(()) } @@ -167,8 +232,20 @@ pub(super) fn get_original_resolution( .into() } -pub fn to_display_info(all: &Vec) -> Vec { - all.iter() +#[cfg(target_os = "linux")] +pub(super) fn get_sync_displays() -> Vec { + SYNC_DISPLAYS.lock().unwrap().displays.clone() +} + +pub(super) fn get_display_info(idx: usize) -> Option { + SYNC_DISPLAYS.lock().unwrap().displays.get(idx).cloned() +} + +// Display to DisplayInfo +// The DisplayInfo is be sent to the peer. +pub(super) fn check_update_displays(all: &Vec) { + let displays = all + .iter() .map(|d| { let display_name = d.name(); let original_resolution = get_original_resolution(&display_name, d.width(), d.height()); @@ -184,32 +261,34 @@ pub fn to_display_info(all: &Vec) -> Vec { ..Default::default() } }) - .collect::>() + .collect::>(); + SYNC_DISPLAYS.lock().unwrap().check_changed(displays); } pub fn is_inited_msg() -> Option { #[cfg(target_os = "linux")] - if !scrap::is_x11() { + if !is_x11() { return super::wayland::is_inited(); } None } -pub async fn get_displays() -> ResultType> { +pub async fn update_get_sync_displays() -> ResultType> { #[cfg(target_os = "linux")] { - if !scrap::is_x11() { + if !is_x11() { return super::wayland::get_displays().await; } } - Ok(to_display_info(&try_get_displays()?)) + check_update_displays(&try_get_displays()?); + Ok(SYNC_DISPLAYS.lock().unwrap().displays.clone()) } #[inline] pub fn get_primary() -> usize { #[cfg(target_os = "linux")] { - if !scrap::is_x11() { + if !is_x11() { return match super::wayland::get_primary() { Ok(n) => n, Err(_) => 0, diff --git a/src/server/input_service.rs b/src/server/input_service.rs index d40bf02c1..a90a0005d 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1,8 +1,6 @@ use super::*; #[cfg(target_os = "macos")] use crate::common::is_server; -#[cfg(target_os = "linux")] -use crate::common::IS_X11; use crate::input::*; #[cfg(target_os = "macos")] use dispatch::Queue; @@ -1152,7 +1150,7 @@ fn map_keyboard_mode(evt: &KeyEvent) { // Wayland #[cfg(target_os = "linux")] - if !*IS_X11 { + if !crate::platform::linux::is_x11() { let mut en = ENIGO.lock().unwrap(); let code = evt.chr() as u16; diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 1dd793fc3..d5f39a2c6 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -18,7 +18,16 @@ // to-do: // https://slhck.info/video/2017/03/01/rate-control.html -use super::{service::ServiceTmpl, video_qos::VideoQoS, *}; +use super::{ + display_service::{check_display_changed, get_display_info}, + service::ServiceTmpl, + video_qos::VideoQoS, + *, +}; +#[cfg(target_os = "linux")] +use crate::common::SimpleCallOnReturn; +#[cfg(target_os = "linux")] +use crate::platform::linux::is_x11; #[cfg(windows)] use crate::{platform::windows::is_process_consent_running, privacy_win_mag}; use hbb_common::{ @@ -37,8 +46,6 @@ use scrap::{ vpxcodec::{VpxEncoderConfig, VpxVideoCodecId}, CodecName, Display, TraitCapturer, }; -#[cfg(target_os = "linux")] -use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(windows)] use std::sync::Once; use std::{ @@ -49,7 +56,6 @@ use std::{ }; pub const NAME: &'static str = "video"; -pub const OPTION_DISPLAY_CHANGED: &'static str = "changed"; pub const OPTION_REFRESH: &'static str = "refresh"; lazy_static::lazy_static! { @@ -63,10 +69,6 @@ lazy_static::lazy_static! { pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc> = Default::default(); } -// https://github.com/rustdesk/rustdesk/discussions/6042, avoiding dbus call -#[cfg(target_os = "linux")] -static IS_X11: AtomicBool = AtomicBool::new(false); - #[inline] pub fn notify_video_frame_fetched(conn_id: i32, frame_tm: Option) { FRAME_FETCHED_NOTIFIER.0.send((conn_id, frame_tm)).ok(); @@ -165,35 +167,6 @@ pub fn new(idx: usize) -> GenericService { vs.sp } -fn check_display_changed( - last_n: usize, - last_current: usize, - last_width: usize, - last_height: usize, -) -> bool { - #[cfg(target_os = "linux")] - { - // wayland do not support changing display for now - if !IS_X11.load(Ordering::SeqCst) { - return false; - } - } - - let displays = match try_get_displays() { - Ok(d) => d, - _ => return false, - }; - - let n = displays.len(); - if n != last_n { - return true; - }; - match displays.get(last_current) { - Some(d) => d.width() != last_width || d.height() != last_height, - None => true, - } -} - // Capturer object is expensive, avoiding to create it frequently. fn create_capturer( privacy_mode_id: i32, @@ -323,7 +296,6 @@ fn check_uac_switch(privacy_mode_id: i32, capturer_privacy_mode_id: i32) -> Resu } pub(super) struct CapturerInfo { - pub name: String, pub origin: (i32, i32), pub width: usize, pub height: usize, @@ -355,7 +327,7 @@ fn get_capturer( ) -> ResultType { #[cfg(target_os = "linux")] { - if !IS_X11.load(Ordering::SeqCst) { + if !is_x11() { return super::wayland::get_capturer(); } } @@ -415,7 +387,6 @@ fn get_capturer( portable_service_running, )?; Ok(CapturerInfo { - name, origin, width, height, @@ -431,16 +402,21 @@ fn run(vs: VideoService) -> ResultType<()> { #[cfg(not(any(target_os = "android", target_os = "ios")))] let _wake_lock = get_wake_lock(); - #[cfg(target_os = "linux")] - { - IS_X11.store(scrap::is_x11(), Ordering::SeqCst); - } - + // Wayland only support one video capturer for now. It is ok to call ensure_inited() here. + // // ensure_inited() is needed because clear() may be called. // to-do: wayland ensure_inited should pass current display index. // But for now, we do not support multi-screen capture on wayland. #[cfg(target_os = "linux")] super::wayland::ensure_inited()?; + #[cfg(target_os = "linux")] + let _wayland_call_on_ret = SimpleCallOnReturn { + b: true, + f: Box::new(|| { + super::wayland::clear(); + }), + }; + #[cfg(windows)] let last_portable_service_running = crate::portable_service::client::running(); #[cfg(not(windows))] @@ -458,8 +434,7 @@ fn run(vs: VideoService) -> ResultType<()> { log::info!("init quality={:?}, abr enabled:{}", quality, abr); let codec_name = Encoder::negotiated_codec(); let recorder = get_recorder(c.width, c.height, &codec_name); - let last_recording = - (recorder.lock().unwrap().is_some() || video_qos.record()) && codec_name != CodecName::AV1; + let last_recording = recorder.lock().unwrap().is_some() || video_qos.record(); drop(video_qos); let encoder_cfg = get_encoder_config(&c, quality, last_recording); @@ -471,16 +446,6 @@ fn run(vs: VideoService) -> ResultType<()> { c.set_use_yuv(encoder.use_yuv()); VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate()); - if sp.is_option_true(OPTION_DISPLAY_CHANGED) { - log::debug!("Broadcasting display changed"); - broadcast_display_changed( - display_idx, - &sp, - Some((c.name.clone(), c.origin.clone(), c.width, c.height)), - ); - sp.set_option_bool(OPTION_DISPLAY_CHANGED, false); - } - if sp.is_option_true(OPTION_REFRESH) { sp.set_option_bool(OPTION_REFRESH, false); } @@ -511,14 +476,14 @@ fn run(vs: VideoService) -> ResultType<()> { allow_err!(encoder.set_quality(quality)); video_qos.store_bitrate(encoder.bitrate()); } - let recording = (recorder.lock().unwrap().is_some() || video_qos.record()) - && codec_name != CodecName::AV1; + let recording = recorder.lock().unwrap().is_some() || video_qos.record(); if recording != last_recording { bail!("SWITCH"); } drop(video_qos); - if sp.is_option_true(OPTION_DISPLAY_CHANGED) || sp.is_option_true(OPTION_REFRESH) { + if sp.is_option_true(OPTION_REFRESH) { + let _ = try_broadcast_display_changed(&sp, display_idx, &c); bail!("SWITCH"); } if codec_name != Encoder::negotiated_codec() { @@ -540,14 +505,9 @@ fn run(vs: VideoService) -> ResultType<()> { let now = time::Instant::now(); if last_check_displays.elapsed().as_millis() > 1000 { last_check_displays = now; - - // Capturer on macos does not return Err event the solution is changed. - #[cfg(target_os = "macos")] - if check_display_changed(c.ndisplay, c.current, c.width, c.height) { - sp.set_option_bool(OPTION_DISPLAY_CHANGED, true); - log::info!("Displays changed"); - bail!("SWITCH"); - } + // This check may be redundant, but it is better to be safe. + // The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough. + try_broadcast_display_changed(&sp, display_idx, &c)?; } frame_controller.reset(); @@ -609,7 +569,7 @@ fn run(vs: VideoService) -> ResultType<()> { #[cfg(target_os = "linux")] { would_block_count += 1; - if !IS_X11.load(Ordering::SeqCst) { + if !is_x11() { if would_block_count >= 100 { // to-do: Unknown reason for WouldBlock 100 times (seconds = 100 * 1 / fps) // https://github.com/rustdesk/rustdesk/blob/63e6b2f8ab51743e77a151e2b7ff18816f5fa2fb/libs/scrap/src/common/wayland.rs#L81 @@ -624,13 +584,9 @@ fn run(vs: VideoService) -> ResultType<()> { } } Err(err) => { - if check_display_changed(c.ndisplay, c.current, c.width, c.height) { - log::info!("Displays changed"); - #[cfg(target_os = "linux")] - super::wayland::clear(); - sp.set_option_bool(OPTION_DISPLAY_CHANGED, true); - bail!("SWITCH"); - } + // This check may be redundant, but it is better to be safe. + // The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough. + try_broadcast_display_changed(&sp, display_idx, &c)?; #[cfg(windows)] if !c.is_gdi() { @@ -638,7 +594,6 @@ fn run(vs: VideoService) -> ResultType<()> { log::info!("dxgi error, fall back to gdi: {:?}", err); continue; } - return Err(err.into()); } _ => { @@ -671,9 +626,6 @@ fn run(vs: VideoService) -> ResultType<()> { } } - #[cfg(target_os = "linux")] - super::wayland::clear(); - Ok(()) } @@ -797,7 +749,7 @@ fn handle_one_frame( pub fn is_inited_msg() -> Option { #[cfg(target_os = "linux")] - if !IS_X11.load(Ordering::SeqCst) { + if !is_x11() { return super::wayland::is_inited(); } None @@ -889,58 +841,52 @@ fn get_wake_lock() -> crate::platform::WakeLock { } #[inline] -fn broadcast_display_changed( - display_idx: usize, +fn try_broadcast_display_changed( sp: &GenericService, - display_meta: Option<(String, (i32, i32), usize, usize)>, -) { - if let Some(msg_out) = make_display_changed_msg(display_idx, display_meta) { - sp.send(msg_out); - } -} - -fn get_display_info_simple_meta(display_idx: usize) -> Option<(String, (i32, i32), usize, usize)> { - let displays = display_service::try_get_displays().ok()?; - if let Some(display) = displays.get(display_idx) { - Some(( - display.name(), - display.origin(), - display.width(), - display.height(), - )) - } else { - None + display_idx: usize, + cap: &CapturerInfo, +) -> ResultType<()> { + if let Some(display) = check_display_changed( + cap.ndisplay, + cap.current, + (cap.origin.0, cap.origin.1, cap.width, cap.height), + ) { + log::info!("Display {} changed", display); + if let Some(msg_out) = make_display_changed_msg(display_idx, Some(display)) { + sp.send(msg_out); + bail!("SWITCH"); + } } + Ok(()) } pub fn make_display_changed_msg( display_idx: usize, - display_meta: Option<(String, (i32, i32), usize, usize)>, + opt_display: Option, ) -> Option { - let mut misc = Misc::new(); - let (name, origin, width, height) = match display_meta { + let display = match opt_display { Some(d) => d, - None => get_display_info_simple_meta(display_idx)?, + None => get_display_info(display_idx)?, }; - let original_resolution = display_service::get_original_resolution(&name, width, height); + let mut misc = Misc::new(); misc.set_switch_display(SwitchDisplay { display: display_idx as _, - x: origin.0, - y: origin.1, - width: width as _, - height: height as _, + x: display.x, + y: display.y, + width: display.width, + height: display.height, cursor_embedded: display_service::capture_cursor_embedded(), #[cfg(not(any(target_os = "android", target_os = "ios")))] resolutions: Some(SupportedResolutions { - resolutions: if name.is_empty() { + resolutions: if display.name.is_empty() { vec![] } else { - crate::platform::resolutions(&name) + crate::platform::resolutions(&display.name) }, ..SupportedResolutions::default() }) .into(), - original_resolution, + original_resolution: display.original_resolution, ..Default::default() }); let mut msg_out = Message::new(); diff --git a/src/server/wayland.rs b/src/server/wayland.rs index f26b27b20..f869266bc 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -4,8 +4,11 @@ use scrap::{is_cursor_embedded, set_map_err, Capturer, Display, Frame, TraitCapt use std::io; use std::process::{Command, Output}; -use crate::client::{ - SCRAP_OTHER_VERSION_OR_X11_REQUIRED, SCRAP_UBUNTU_HIGHER_REQUIRED, SCRAP_X11_REQUIRED, +use crate::{ + client::{ + SCRAP_OTHER_VERSION_OR_X11_REQUIRED, SCRAP_UBUNTU_HIGHER_REQUIRED, SCRAP_X11_REQUIRED, + }, + platform::linux::is_x11, }; lazy_static::lazy_static! { @@ -96,7 +99,7 @@ pub(super) async fn ensure_inited() -> ResultType<()> { } pub(super) fn is_inited() -> Option { - if scrap::is_x11() { + if is_x11() { None } else { if *CAP_DISPLAY_INFO.read().unwrap() == 0 { @@ -133,7 +136,7 @@ fn get_max_desktop_resolution() -> Option { } pub(super) async fn check_init() -> ResultType<()> { - if !scrap::is_x11() { + if !is_x11() { let mut minx = 0; let mut maxx = 0; let mut miny = 0; @@ -146,7 +149,8 @@ pub(super) async fn check_init() -> ResultType<()> { let num = all.len(); let primary = super::display_service::get_primary_2(&all); let current = primary; - let mut displays = super::display_service::to_display_info(&all); + super::display_service::check_update_displays(&all); + let mut displays = super::display_service::get_sync_displays(); for display in displays.iter_mut() { display.cursor_embedded = is_cursor_embedded(); } @@ -173,12 +177,15 @@ pub(super) async fn check_init() -> ResultType<()> { Some(result) if !result.is_empty() => { let resolution: Vec<&str> = result.split(" ").collect(); let w: i32 = resolution[0].parse().unwrap_or(origin.0 + width as i32); - let h: i32 = resolution[2].trim_end_matches(",").parse().unwrap_or(origin.1 + height as i32); + let h: i32 = resolution[2] + .trim_end_matches(",") + .parse() + .unwrap_or(origin.1 + height as i32); (w, h) } - _ => (origin.0 + width as i32, origin.1 + height as i32) + _ => (origin.0 + width as i32, origin.1 + height as i32), }; - + minx = 0; maxx = max_width; miny = 0; @@ -242,7 +249,7 @@ pub(super) fn get_primary() -> ResultType { } pub fn clear() { - if scrap::is_x11() { + if is_x11() { return; } let mut write_lock = CAP_DISPLAY_INFO.write().unwrap(); @@ -257,7 +264,7 @@ pub fn clear() { } pub(super) fn get_capturer() -> ResultType { - if scrap::is_x11() { + if is_x11() { bail!("Do not call this function if not wayland"); } let addr = *CAP_DISPLAY_INFO.read().unwrap(); @@ -267,9 +274,6 @@ pub(super) fn get_capturer() -> ResultType { let cap_display_info = &*cap_display_info; let rect = cap_display_info.rects[cap_display_info.current]; Ok(super::video_service::CapturerInfo { - name: cap_display_info.displays[cap_display_info.current] - .name - .clone(), origin: rect.0, width: rect.1, height: rect.2, diff --git a/src/tray.rs b/src/tray.rs index 7b05be101..221a8a7fe 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,9 +1,12 @@ -use crate::{client::translate, ipc::Data}; -use hbb_common::{allow_err, log, tokio}; -use std::{ - sync::{Arc, Mutex}, - time::Duration, -}; +use crate::client::translate; +#[cfg(windows)] +use crate::ipc::Data; +#[cfg(windows)] +use hbb_common::tokio; +use hbb_common::{allow_err, log}; +use std::sync::{Arc, Mutex}; +#[cfg(windows)] +use std::time::Duration; pub fn start_tray() { allow_err!(make_tray()); diff --git a/src/ui/header.tis b/src/ui/header.tis index deac05605..e0af9eed7 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -144,7 +144,7 @@ class Header: Reactor.Component { {svg_action} {svg_display} {svg_keyboard} - {recording_enabled && qualityMonitorData[4] != "AV1" ? {recording ? svg_recording_on : svg_recording_off} : ""} + {recording_enabled ? {recording ? svg_recording_on : svg_recording_off} : ""} {this.renderKeyboardPop()} {this.renderDisplayPop()} {this.renderActionPop()} @@ -299,14 +299,22 @@ class Header: Reactor.Component { header.update(); handler.record_status(recording); // 0 is just a dummy value. It will be ignored by the handler. - if (recording) + if (recording) { handler.refresh_video(0); - else - handler.record_screen(false, 0, display_width, display_height); + if (handler.version_cmp(pi.version, '1.2.4') >= 0) handler.record_screen(recording, pi.current_display, display_width, display_height); + } + else { + handler.record_screen(recording, pi.current_display, display_width, display_height); + } } event click $(#screen) (_, me) { if (pi.current_display == me.index) return; + if (recording) { + recording = false; + handler.record_screen(false, pi.current_display, display_width, display_height); + handler.record_status(false); + } handler.switch_display(me.index); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 71ef8f84d..bb01ef9f4 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -243,6 +243,7 @@ impl InvokeUiSession for SciterHandler { pi_sciter.set_item("sas_enabled", pi.sas_enabled); pi_sciter.set_item("displays", Self::make_displays_array(&pi.displays)); pi_sciter.set_item("current_display", pi.current_display); + pi_sciter.set_item("version", pi.version.clone()); self.call("updatePi", &make_args!(pi_sciter)); } @@ -469,6 +470,7 @@ impl sciter::EventHandler for SciterSession { fn restart_remote_device(); fn request_voice_call(); fn close_voice_call(); + fn version_cmp(String, String); } } @@ -757,6 +759,10 @@ impl SciterSession { log::error!("Failed to spawn IP tunneling: {}", err); } } + + fn version_cmp(&self, v1: String, v2: String) -> i32 { + (hbb_common::get_version_number(&v1) - hbb_common::get_version_number(&v2)) as i32 + } } pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 022d43668..a99cd188b 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -522,7 +522,6 @@ handler.updateQualityStatus = function(speed, fps, delay, bitrate, codec_format) bitrate ? qualityMonitorData[3] = bitrate:null; codec_format ? qualityMonitorData[4] = codec_format:null; qualityMonitor.update(); - if (codec_format) header.update(); } handler.setPermission = function(name, enabled) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index e6ed96af8..12fc82528 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -240,6 +240,10 @@ impl Session { self.lc.read().unwrap().displays_as_individual_windows.clone() } + pub fn get_use_all_my_displays_for_the_remote_session(&self) -> String { + self.lc.read().unwrap().use_all_my_displays_for_the_remote_session.clone() + } + pub fn save_reverse_mouse_wheel(&self, value: String) { self.lc.write().unwrap().save_reverse_mouse_wheel(value); } @@ -248,6 +252,10 @@ impl Session { self.lc.write().unwrap().save_displays_as_individual_windows(value); } + pub fn save_use_all_my_displays_for_the_remote_session(&self, value: String) { + self.lc.write().unwrap().save_use_all_my_displays_for_the_remote_session(value); + } + pub fn save_view_style(&self, value: String) { self.lc.write().unwrap().save_view_style(value); }