diff --git a/Cargo.lock b/Cargo.lock
index daa2a205b..337a4a695 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4121,9 +4121,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.17.1"
+version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "opaque-debug"
@@ -6736,18 +6736,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..65af2e8a4 100644
--- a/docs/README-CS.md
+++ b/docs/README-CS.md
@@ -6,10 +6,10 @@
Struktura •
Ukázky
[English] | [Українська] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
- Potřebujeme Vaši pomoc s překláním textů tohoto ČTIMNE, uživatelského rozhraní aplikace RustDesk a dokumentace k ní do vašeho jazyka
+ Potřebujeme Vaši pomoc s překladem tohoto README, uživatelského rozhraní aplikace RustDesk a dokumentace k ní do vašeho jazyka
-Dopisujte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Popovídejte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[](https://ko-fi.com/I2I04VU09)
@@ -44,7 +44,7 @@ Varianta pro mobilní platformy používá aplikační rámec (framework) Flutte
- Připravte si vývojové prostředí pro jazyky Rust a C++
-- Nainstalujte [vcpkg](https://github.com/microsoft/vcpkg), a nastavte správně proměnnou prostsředí `VCPKG_ROOT`
+- Nainstalujte [vcpkg](https://github.com/microsoft/vcpkg), a správně nastavte proměnnou prostředí `VCPKG_ROOT`
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
@@ -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:
@@ -131,7 +127,7 @@ Poté pokaždé, když bude třeba aplikaci sestavit, spusťte následující p
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
```
-Všimněte si, že prvotní sestavení může trvat déle (než se do mezipaměti uloží veškeré softwarové součásti, které jsou potřeba) – následná opakování už budou rychlejší. Dále, pokud potřebujete příkazu pro sestavení zadat nějaké argumenty, je možné je zapsat na konec příkazu na pozici ``. Například, pokud byste chtěli sestavit optimalizovaně pro vydání, spustili byste výše uvedený příkaz následovaný `--release`. Výsledný spustitelný soubor se objeví v cílové složce na vašem systému a bude ho možné spustit pomocí:
+Všimněte si, že prvotní sestavení může trvat déle (než se do mezipaměti uloží veškeré softwarové součásti, které jsou potřeba) – následná opakování už budou rychlejší. Pokud navíc potřebujete zadat různé argumenty příkazu pro sestavení, můžete tak učinit na konci příkazu v pozici ``. Například, pokud byste chtěli sestavit optimalizovanou verzi pro vydání, spustili byste výše uvedený příkaz následovaný `--release`. Výsledný spustitelný soubor se objeví v cílové složce na vašem systému a bude ho možné spustit pomocí:
```sh
target/debug/rustdesk
@@ -143,7 +139,7 @@ Nebo, pokud spouštíte variantu pro vydání:
target/release/rustdesk
```
-Zajistětě, abyste tyto příkazy spouštěli z kořene repozitáře s RustDesk, jinak aplikace nemusí být schopná nalézt potřebné prostředky (resources). Také si všimněte, že ostatní dílčí príkazy nástroje cargo, jako třeba `install` nebo `run` zatím nejsou prostřednictvím této metody podporovány, protože by vedly k instalaci či spuštění program uvnitř kontejneru namísto přímo v systému.
+Ujistěte se, že tyto příkazy spouštíte z kořenového adresáře RustDesk, jinak aplikace nemusí být schopná nalézt potřebné prostředky (resources). Také si všimněte, že ostatní dílčí príkazy nástroje cargo, jako třeba `install` nebo `run` zatím nejsou prostřednictvím této metody podporovány, protože by vedly k instalaci či spuštění program uvnitř kontejneru namísto přímo v systému.
## Struktura souborů
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..55e4dbcc1
--- /dev/null
+++ b/flutter/lib/common/widgets/autocomplete.dart
@@ -0,0 +1,191 @@
+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 (!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 VoidCallback onSelect;
+ final Peer peer;
+
+ const AutocompletePeerTile({
+ Key? key,
+ required this.onSelect,
+ 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: () => widget.onSelect(),
+ 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..b10fe3e7d 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,25 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
onVisibilityChanged: onVisibilityChanged,
child: widget.peerCardBuilder(peer),
);
+ final windowWidth = MediaQuery.of(context).size.width;
+ // `Provider.of(context)` will causes infinete loop.
+ // Because `gFFI.peerTabModel.setCurrentTabCachedPeers(peers)` will trigger `notifyListeners()`.
+ //
+ // No need to listen the currentTab change event.
+ // Because the currentTab change event will trigger the peers change event,
+ // and the peers change event will trigger _buildPeersView().
+ final currentTab = Provider.of(context, listen: false).currentTab;
+ final hideAbTagsPanel = bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
return isDesktop
? Obx(
() => SizedBox(
- width: 220,
+ width: peerCardUiType.value != PeerUiType.list
+ ? 220
+ : currentTab == PeerTabIndex.group.index || (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 ebaff8954..28b10785b 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: [
@@ -535,5 +532,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..073edbfec 100644
--- a/flutter/lib/consts.dart
+++ b/flutter/lib/consts.dart
@@ -3,6 +3,10 @@ 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 int kMaxVirtualDisplayCount = 4;
+const int kAllVirtualDisplay = -1;
const double kDesktopRemoteTabBarHeight = 28.0;
const int kInvalidWindowId = -1;
@@ -10,6 +14,15 @@ const int kMainWindowId = 0;
const kAllDisplayValue = -1;
+const kKeyLegacyMode = 'legacy';
+const kKeyMapMode = 'map';
+const kKeyTranslateMode = 'translate';
+
+const String kPlatformAdditionsIsWayland = "is_wayland";
+const String kPlatformAdditionsHeadless = "headless";
+const String kPlatformAdditionsIsInstalled = "is_installed";
+const String kPlatformAdditionsVirtualDisplays = "virtual_displays";
+
const String kPeerPlatformWindows = "Windows";
const String kPeerPlatformLinux = "Linux";
const String kPeerPlatformMacOS = "Mac OS";
@@ -29,6 +42,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 +78,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 +101,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..bf923d388 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,133 @@ 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;
+ },
+ ));
+ },
+ onSelected: (option) {
+ setState(() {
+ _idController.id = option.id;
+ FocusScope.of(context).unfocus();
+ });
+ },
+ 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(onSelect: () => onSelected(peer), 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..d8b330407 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;
@@ -1014,6 +978,10 @@ class _DisplayMenuState extends State<_DisplayMenu> {
ffi: widget.ffi,
screenAdjustor: _screenAdjustor,
),
+ _VirtualDisplayMenu(
+ id: widget.id,
+ ffi: widget.ffi,
+ ),
Divider(),
toggles(),
widget.pluginItem,
@@ -1423,6 +1391,70 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
}
}
+class _VirtualDisplayMenu extends StatefulWidget {
+ final String id;
+ final FFI ffi;
+
+ _VirtualDisplayMenu({
+ Key? key,
+ required this.id,
+ required this.ffi,
+ }) : super(key: key);
+
+ @override
+ State<_VirtualDisplayMenu> createState() => _VirtualDisplayMenuState();
+}
+
+class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
+ return Offstage();
+ }
+ if (!widget.ffi.ffiModel.pi.isInstalled) {
+ return Offstage();
+ }
+
+ final virtualDisplays = widget.ffi.ffiModel.pi.virtualDisplays;
+
+ final children = [];
+ for (var i = 0; i < kMaxVirtualDisplayCount; i++) {
+ children.add(CkbMenuButton(
+ value: virtualDisplays.contains(i + 1),
+ onChanged: (bool? value) async {
+ if (value != null) {
+ bind.sessionToggleVirtualDisplay(
+ sessionId: widget.ffi.sessionId, index: i + 1, on: value);
+ }
+ },
+ child: Text('${translate('Virtual display')} ${i + 1}'),
+ ffi: widget.ffi,
+ ));
+ }
+ children.add(Divider());
+ children.add(MenuButton(
+ onPressed: () {
+ bind.sessionToggleVirtualDisplay(
+ sessionId: widget.ffi.sessionId,
+ index: kAllVirtualDisplay,
+ on: false);
+ },
+ ffi: widget.ffi,
+ child: Text(translate('Plug out all')),
+ ));
+ return _SubmenuButton(
+ ffi: widget.ffi,
+ menuChildren: children,
+ child: Text(translate("Virtual display")),
+ );
+ }
+}
+
class _KeyboardMenu extends StatelessWidget {
final String id;
final FFI ffi;
@@ -1438,18 +1470,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 +1501,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 +1525,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 +1707,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 +1730,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 +1744,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 +2112,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..a4e3c7f4d 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,42 @@ class _ConnectionPageState extends State {
color: MyTheme.darkGray,
),
),
- controller: _idController,
inputFormatters: [IDTextInputFormatter()],
+ );
+ },
+ onSelected: (option) {
+ setState(() {
+ _idController.id = option.id;
+ FocusScope.of(context).unfocus();
+ });
+ },
+ 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(onSelect: () => onSelected(peer), peer: peer)).toList(),
+ ))))
+ );
+ },
),
),
),
@@ -170,7 +285,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 3346e03de..99d91c4c4 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(
@@ -767,7 +767,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..0aad54b37 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,9 +245,11 @@ 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 == 'sync_platform_additions') {
+ handlePlatformAdditions(evt, sessionId, peerId);
} else if (name == 'connection_ready') {
setConnectionType(
peerId, evt['secure'] == 'true', evt['direct'] == 'true');
@@ -430,14 +432,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 +514,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 +532,7 @@ class FfiModel with ChangeNotifier {
bool forceRelay) {
bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay);
clearPermissions();
+ dialogManager.dismissAll();
dialogManager.showLoading(translate('Connecting...'),
onCancel: closeConnection);
}
@@ -623,7 +626,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 +692,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 +705,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 +862,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?
@@ -814,8 +897,37 @@ class FfiModel with ChangeNotifier {
notifyListeners();
}
+ handlePlatformAdditions(
+ Map evt, SessionID sessionId, String peerId) async {
+ final updateData = evt['platform_additions'] as String?;
+ if (updateData == null) {
+ return;
+ }
+
+ if (updateData.isEmpty) {
+ _pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays);
+ } else {
+ try {
+ final updateJson = json.decode(updateData);
+ for (final key in updateJson.keys) {
+ _pi.platformAdditions[key] = updateJson[key];
+ }
+ if (!updateJson.contains(kPlatformAdditionsVirtualDisplays)) {
+ _pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays);
+ }
+ } catch (e) {
+ debugPrint('Failed to decode platformAdditions $e');
+ }
+ }
+
+ cachedPeerData.peerInfo['platform_additions'] =
+ json.encode(_pi.platformAdditions);
+ }
+
// 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 +936,6 @@ class FfiModel with ChangeNotifier {
} catch (e) {
//
}
- parent.target?.recordingModel.onSwitchDisplay();
}
updateBlockInputState(Map evt, String peerId) {
@@ -1806,57 +1917,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 +2324,18 @@ 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[kPlatformAdditionsIsWayland] == true;
+ bool get isHeadless => platformAdditions[kPlatformAdditionsHeadless] == true;
+ bool get isInstalled =>
+ platform != kPeerPlatformWindows ||
+ platformAdditions[kPlatformAdditionsIsInstalled] == true;
+ List get virtualDisplays => List.from(
+ platformAdditions[kPlatformAdditionsVirtualDisplays] ?? []);
bool get isSupportMultiDisplay => isDesktop && isSupportMultiUiSession;
@@ -2237,7 +2363,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/protos/message.proto b/libs/hbb_common/protos/message.proto
index bba23ff70..454bc0d3d 100644
--- a/libs/hbb_common/protos/message.proto
+++ b/libs/hbb_common/protos/message.proto
@@ -102,6 +102,7 @@ message PeerInfo {
SupportedEncoding encoding = 10;
SupportedResolutions resolutions = 11;
// Use JSON's key-value format which is friendly for peer to handle.
+ // NOTE: Only support one-level dictionaries (for peer to update), and the key is of type string.
string platform_additions = 12;
}
@@ -498,6 +499,11 @@ message CaptureDisplays {
repeated int32 set = 3;
}
+message ToggleVirtualDisplay {
+ int32 display = 1;
+ bool on = 2;
+}
+
message PermissionInfo {
enum Permission {
Keyboard = 0;
@@ -697,6 +703,7 @@ message Misc {
bool client_record_status = 29;
CaptureDisplays capture_displays = 30;
int32 refresh_video_display = 31;
+ ToggleVirtualDisplay toggle_virtual_display = 32;
}
}
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/libs/virtual_display/dylib/src/lib.rs b/libs/virtual_display/dylib/src/lib.rs
index f2997da16..573c51810 100644
--- a/libs/virtual_display/dylib/src/lib.rs
+++ b/libs/virtual_display/dylib/src/lib.rs
@@ -1,9 +1,10 @@
#[cfg(windows)]
pub mod win10;
+use hbb_common::ResultType;
#[cfg(windows)]
-use hbb_common::lazy_static;
-use hbb_common::{bail, ResultType};
-use std::path::Path;
+use hbb_common::{bail, lazy_static};
+#[cfg(windows)]
+use std::path::PathBuf;
#[cfg(windows)]
use std::sync::Mutex;
@@ -33,18 +34,25 @@ pub fn download_driver() -> ResultType<()> {
Ok(())
}
-#[no_mangle]
-pub fn install_update_driver(_reboot_required: &mut bool) -> ResultType<()> {
- #[cfg(windows)]
+#[cfg(windows)]
+fn get_driver_install_abs_path() -> ResultType {
let install_path = win10::DRIVER_INSTALL_PATH;
- #[cfg(not(windows))]
- let install_path = "";
-
- let abs_path = Path::new(install_path).canonicalize()?;
+ let exe_file = std::env::current_exe()?;
+ let abs_path = match exe_file.parent() {
+ Some(cur_dir) => cur_dir.join(install_path),
+ None => bail!(
+ "Invalid exe parent for {}",
+ exe_file.to_string_lossy().as_ref()
+ ),
+ };
if !abs_path.exists() {
bail!("{} not exists", install_path)
}
+ Ok(abs_path)
+}
+#[no_mangle]
+pub fn install_update_driver(_reboot_required: &mut bool) -> ResultType<()> {
#[cfg(windows)]
unsafe {
{
@@ -54,6 +62,7 @@ pub fn install_update_driver(_reboot_required: &mut bool) -> ResultType<()> {
bail!("{}", e);
}
+ let abs_path = get_driver_install_abs_path()?;
let full_install_path: Vec = abs_path
.to_string_lossy()
.as_ref()
@@ -76,19 +85,10 @@ pub fn install_update_driver(_reboot_required: &mut bool) -> ResultType<()> {
#[no_mangle]
pub fn uninstall_driver(_reboot_required: &mut bool) -> ResultType<()> {
- #[cfg(windows)]
- let install_path = win10::DRIVER_INSTALL_PATH;
- #[cfg(not(windows))]
- let install_path = "";
-
- let abs_path = Path::new(install_path).canonicalize()?;
- if !abs_path.exists() {
- bail!("{} not exists", install_path)
- }
-
#[cfg(windows)]
unsafe {
{
+ let abs_path = get_driver_install_abs_path()?;
let full_install_path: Vec = abs_path
.to_string_lossy()
.as_ref()
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/client/io_loop.rs b/src/client/io_loop.rs
index 4c830a2f9..db7a4c5b7 100644
--- a/src/client/io_loop.rs
+++ b/src/client/io_loop.rs
@@ -1531,6 +1531,7 @@ impl Remote {
}
Some(message::Union::PeerInfo(pi)) => {
self.handler.set_displays(&pi.displays);
+ self.handler.set_platform_additions(&pi.platform_additions);
}
_ => {}
}
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..395056038 100644
--- a/src/flutter.rs
+++ b/src/flutter.rs
@@ -691,6 +691,13 @@ impl InvokeUiSession for FlutterHandler {
);
}
+ fn set_platform_additions(&self, data: &str) {
+ self.push_event(
+ "sync_platform_additions",
+ vec![("platform_additions", &data)],
+ )
+ }
+
fn on_connected(&self, _conn_type: ConnType) {}
fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) {
@@ -1377,6 +1384,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 +1451,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 8769106ef..0f692ee53 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