Merge remote-tracking branch 'upstream/master'
# Conflicts: # src/server/connection.rs
This commit is contained in:
commit
7b24835c9e
14
Cargo.lock
generated
14
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
55
Dockerfile
55
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"]
|
||||
|
34
README.md
34
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
|
||||
|
@ -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 طريقة البناء باستخدام
|
||||
|
||||
ابدأ باستنساخ المستودع وبناء الكونتاينر:
|
||||
|
@ -6,10 +6,10 @@
|
||||
<a href="#file-structure">Struktura</a> •
|
||||
<a href="#snapshot">Ukázky</a><br>
|
||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||
<b>Potřebujeme Vaši pomoc s překláním textů tohoto ČTIMNE, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">uživatelského rozhraní aplikace RustDesk</a> a <a href="https://github.com/rustdesk/doc.rustdesk.com">dokumentace k ní</a> do vašeho jazyka</b>
|
||||
<b>Potřebujeme Vaši pomoc s překladem tohoto README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">uživatelského rozhraní aplikace RustDesk</a> a <a href="https://github.com/rustdesk/doc.rustdesk.com">dokumentace k ní</a> do vašeho jazyka</b>
|
||||
</p>
|
||||
|
||||
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)
|
||||
|
||||
|
||||
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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 `<OPTIONAL-ARGS>`. 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 `<OPTIONAL-ARGS>`. 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ů
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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 را دریافت کنید و کانتینر را به روش زیر بسازید
|
||||
|
@ -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ö:
|
||||
|
@ -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 :
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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コンテナを構築することから始めます。
|
||||
|
@ -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 컨테이너 구성하는 것으로 시작합니다.
|
||||
|
@ -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/) പരിശോധിക്കുക.
|
||||
|
||||
## ഡോക്കർ ഉപയോഗിച്ച് എങ്ങനെ നിർമ്മിക്കാം
|
||||
|
||||
റെപ്പോസിറ്റോറി ക്ലോണുചെയ്ത് ഡോക്കർ കണ്ടെയ്നർ നിർമ്മിക്കുന്നതിലൂടെ ആരംഭിക്കുക:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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-контейнера:
|
||||
|
@ -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:
|
||||
|
@ -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-контейнера:
|
||||
|
@ -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:
|
||||
|
@ -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 容器:
|
||||
|
@ -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<Widget> 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<void> 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<Offset?> _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<bool> 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<bool> 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<IDTextEditingController>();
|
||||
idController.text = formatID(id);
|
||||
}
|
||||
if (Get.isRegistered<TextEditingController>()){
|
||||
final fieldTextEditingController = Get.find<TextEditingController>();
|
||||
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<List<Rect>> getScreenListWayland() async {
|
||||
final screenRectList = <Rect>[];
|
||||
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<dynamic>) {
|
||||
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<List<Rect>> getScreenListNotWayland() async {
|
||||
final screenRectList = <Rect>[];
|
||||
final displays = bind.mainGetDisplays();
|
||||
if (displays.isEmpty) {
|
||||
return screenRectList;
|
||||
}
|
||||
try {
|
||||
for (var display in jsonDecode(displays) as List<dynamic>) {
|
||||
// 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<List<Rect>> 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<String, dynamic> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
191
flutter/lib/common/widgets/autocomplete.dart
Normal file
191
flutter/lib/common/widgets/autocomplete.dart
Normal file
@ -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<List<Peer>> getAllPeers() async {
|
||||
Map<String, dynamic> recentPeers = jsonDecode(await bind.mainLoadRecentPeersSync());
|
||||
Map<String, dynamic> lanPeers = jsonDecode(await bind.mainLoadLanPeersSync());
|
||||
Map<String, dynamic> abPeers = jsonDecode(await bind.mainLoadAbSync());
|
||||
Map<String, dynamic> groupPeers = jsonDecode(await bind.mainLoadGroupSync());
|
||||
|
||||
Map<String, dynamic> combinedPeers = {};
|
||||
|
||||
void _mergePeers(Map<String, dynamic> 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<Peer> 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<AutocompletePeerTile>{
|
||||
List _frontN<T>(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),
|
||||
),
|
||||
)
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ import 'dart:math' as math;
|
||||
typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
|
||||
Function(BuildContext);
|
||||
|
||||
enum PeerUiType { grid, list }
|
||||
enum PeerUiType { grid, tile, list }
|
||||
|
||||
final peerCardUiType = PeerUiType.grid.obs;
|
||||
|
||||
|
@ -215,29 +215,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
||||
}
|
||||
|
||||
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<PeerSearchBar> {
|
||||
}
|
||||
}
|
||||
|
||||
class PeerViewDropdown extends StatefulWidget {
|
||||
const PeerViewDropdown({super.key});
|
||||
|
||||
@override
|
||||
State<PeerViewDropdown> createState() => _PeerViewDropdownState();
|
||||
}
|
||||
|
||||
class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
||||
RelativeRect menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<PeerUiType> types = [PeerUiType.grid, PeerUiType.tile, PeerUiType.list];
|
||||
final style = TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge?.color,
|
||||
fontSize: MenuConfig.fontSize,
|
||||
fontWeight: FontWeight.normal);
|
||||
List<PopupMenuEntry> 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<PeerUiType>(
|
||||
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});
|
||||
|
||||
|
@ -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<PeerTabModel>(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<PeerTabModel>(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,
|
||||
),
|
||||
)
|
||||
|
@ -224,11 +224,8 @@ List<TTextMenu> 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<List<TToggleMenu>> 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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<ConnectionPage>
|
||||
Timer? _updateTimer;
|
||||
|
||||
final RxBool _idInputFocused = false.obs;
|
||||
final FocusNode _idFocusNode = FocusNode();
|
||||
|
||||
var svcStopped = Get.find<RxBool>(tag: 'stop-service');
|
||||
var svcIsUsingPublicServer = true.obs;
|
||||
|
||||
bool isWindowMinimized = false;
|
||||
List<Peer> peers = [];
|
||||
List _frontN<T>(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<ConnectionPage>
|
||||
_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<IDTextEditingController>(_idController);
|
||||
windowManager.addListener(this);
|
||||
}
|
||||
@ -76,6 +81,9 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
if (Get.isRegistered<IDTextEditingController>()) {
|
||||
Get.delete<IDTextEditingController>();
|
||||
}
|
||||
if (Get.isRegistered<TextEditingController>()){
|
||||
Get.delete<TextEditingController>();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -142,8 +150,20 @@ class _ConnectionPageState extends State<ConnectionPage>
|
||||
connect(context, id, isFileTransfer: isFileTransfer);
|
||||
}
|
||||
|
||||
Future<void> _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<ConnectionPage>
|
||||
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<Peer>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text == '') {
|
||||
return const Iterable<Peer>.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<TextEditingController>(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<Peer> onSelected, Iterable<Peer> 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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -329,8 +329,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
||||
"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<DesktopHomePage>
|
||||
// });
|
||||
// }
|
||||
} else if (Platform.isLinux) {
|
||||
final LinuxCards = <Widget>[];
|
||||
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<DesktopHomePage>
|
||||
|
||||
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<DesktopHomePage>
|
||||
Get.put<RxBool>(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<DesktopHomePage>
|
||||
} 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<DesktopHomePage>
|
||||
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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ class _RemotePageState extends State<RemotePage>
|
||||
late RxBool _keyboardEnabled;
|
||||
final Map<int, RenderTexture> _renderTextures = {};
|
||||
|
||||
final _blockableOverlayState = BlockableOverlayState();
|
||||
var _blockableOverlayState = BlockableOverlayState();
|
||||
|
||||
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
||||
|
||||
@ -253,9 +253,9 @@ class _RemotePageState extends State<RemotePage>
|
||||
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<RemotePage>
|
||||
},
|
||||
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<RemotePage>
|
||||
: 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<ImagePaint> {
|
||||
} 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
|
||||
|
@ -48,6 +48,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
|
||||
late ToolbarState _toolbarState;
|
||||
String? peerId;
|
||||
bool _isScreenRectSet = false;
|
||||
int? _display;
|
||||
|
||||
var connectionMap = RxList<Widget>.empty(growable: true);
|
||||
|
||||
@ -59,6 +61,10 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
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<ConnectionTabPage> {
|
||||
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<ConnectionTabPage> {
|
||||
_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<ConnectionTabPage> {
|
||||
c++;
|
||||
}
|
||||
}
|
||||
|
||||
loopCloseWindow();
|
||||
}
|
||||
ConnectionTypeState.delete(id);
|
||||
|
@ -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<RemoteToolbar> {
|
||||
|
||||
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<RemoteToolbar> {
|
||||
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<String, bool> 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 = <Widget>[];
|
||||
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<FfiModel>(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<InputModeMenu> 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<RdoMenuButton> 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<String>(
|
||||
@ -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<FfiModel>(context);
|
||||
var ffi = Provider.of<FfiModel>(context);
|
||||
var recordingModel = Provider.of<RecordingModel>(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<QualityMonitorModel>(
|
||||
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;
|
||||
|
@ -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<DesktopTabState> 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<WindowActionPanel>
|
||||
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<void> Function() restoreFunc,
|
||||
Future<bool> Function() checkFullscreen,
|
||||
Future<void> Function() closeFunc) async {
|
||||
await restoreFunc();
|
||||
Future<bool> Function() checkFullscreen,
|
||||
Future<void> Function() closeFunc,
|
||||
) async {
|
||||
_macOSCheckRestoreCounter = 0;
|
||||
_macOSCheckRestoreTimer =
|
||||
Timer.periodic(Duration(milliseconds: 30), (timer) async {
|
||||
@ -612,26 +634,38 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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<ConnectionPage> {
|
||||
|
||||
/// Update url. If it's not null, means an update is available.
|
||||
var _updateUrl = '';
|
||||
List<Peer> peers = [];
|
||||
List _frontN<T>(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<ConnectionPage> {
|
||||
color: Colors.white, fontWeight: FontWeight.bold))));
|
||||
}
|
||||
|
||||
Future<void> _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<ConnectionPage> {
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: AutoSizeTextField(
|
||||
child: Autocomplete<Peer>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (textEditingValue.text == '') {
|
||||
return const Iterable<Peer>.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<ConnectionPage> {
|
||||
color: MyTheme.darkGray,
|
||||
),
|
||||
),
|
||||
controller: _idController,
|
||||
inputFormatters: [IDTextInputFormatter()],
|
||||
);
|
||||
},
|
||||
onSelected: (option) {
|
||||
setState(() {
|
||||
_idController.id = option.id;
|
||||
FocusScope.of(context).unfocus();
|
||||
});
|
||||
},
|
||||
optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<Peer> onSelected, Iterable<Peer> 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<ConnectionPage> {
|
||||
offstage: _idEmpty.value,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
_idController.clear();
|
||||
setState(() {
|
||||
_idController.clear();
|
||||
});
|
||||
},
|
||||
icon: Icon(Icons.clear, color: MyTheme.darkGray)),
|
||||
)),
|
||||
|
@ -235,7 +235,7 @@ class _RemotePageState extends State<RemotePage> {
|
||||
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<RemotePage> {
|
||||
: 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(
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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<String, dynamic> 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<String, dynamic> evt, String peerId) async {
|
||||
handlePeerInfo(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<Display> displays = [];
|
||||
Features features = Features();
|
||||
List<Resolution> resolutions = [];
|
||||
Map<String, dynamic> platformDdditions = {};
|
||||
Map<String, dynamic> 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<int> get virtualDisplays => List<int>.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;
|
||||
|
@ -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<String, Map<int, String?>> _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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -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<int> newSessionWindow(
|
||||
WindowType type, String remoteId, String msg, List<int> windows) async {
|
||||
WindowType type,
|
||||
String remoteId,
|
||||
String msg,
|
||||
List<int> 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<int> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<i32> {
|
||||
let f: f64 = UserDefaultConfig::read()
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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<dyn RecorderApi>,
|
||||
ctx: RecorderContext,
|
||||
pts: Option<i64>,
|
||||
}
|
||||
|
||||
impl Deref for Recorder {
|
||||
@ -101,19 +105,18 @@ impl DerefMut for Recorder {
|
||||
|
||||
impl Recorder {
|
||||
pub fn new(mut ctx: RecorderContext) -> ResultType<Self> {
|
||||
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),
|
||||
|
@ -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<PathBuf> {
|
||||
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<u16> = 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<u16> = abs_path
|
||||
.to_string_lossy()
|
||||
.as_ref()
|
||||
|
@ -999,16 +999,19 @@ pub struct VideoHandler {
|
||||
pub rgb: ImageRgb,
|
||||
recorder: Arc<Mutex<Option<Recorder>>>,
|
||||
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.
|
||||
|
@ -1531,6 +1531,7 @@ impl<T: InvokeUiSession> Remote<T> {
|
||||
}
|
||||
Some(message::Union::PeerInfo(pi)) => {
|
||||
self.handler.set_displays(&pi.displays);
|
||||
self.handler.set_platform_additions(&pi.platform_additions);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@ -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<KeyboardMode> {
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[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<FileEntry>) -> String {
|
||||
use serde_json::json;
|
||||
let mut fd_json = serde_json::Map::new();
|
||||
|
@ -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")))]
|
||||
|
@ -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<FlutterSession> {
|
||||
|
||||
// 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?)
|
||||
}
|
||||
|
@ -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<Option<String>> {
|
||||
pub fn session_get_displays_as_individual_windows(
|
||||
session_id: SessionID,
|
||||
) -> SyncReturn<Option<String>> {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
SyncReturn(Some(session.get_displays_as_individual_windows()))
|
||||
} else {
|
||||
@ -353,6 +360,27 @@ pub fn session_set_displays_as_individual_windows(session_id: SessionID, value:
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_get_use_all_my_displays_for_the_remote_session(
|
||||
session_id: SessionID,
|
||||
) -> SyncReturn<Option<String>> {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
SyncReturn(Some(
|
||||
session.get_use_all_my_displays_for_the_remote_session(),
|
||||
))
|
||||
} else {
|
||||
SyncReturn(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_set_use_all_my_displays_for_the_remote_session(
|
||||
session_id: SessionID,
|
||||
value: String,
|
||||
) {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
session.save_use_all_my_displays_for_the_remote_session(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_get_custom_image_quality(session_id: SessionID) -> Option<Vec<i32>> {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
Some(session.get_custom_image_quality())
|
||||
@ -931,6 +959,25 @@ pub fn main_load_recent_peers_sync() -> SyncReturn<String> {
|
||||
SyncReturn("".to_string())
|
||||
}
|
||||
|
||||
pub fn main_load_lan_peers_sync() -> SyncReturn<String> {
|
||||
let data = HashMap::from([
|
||||
("name", "load_lan_peers".to_owned()),
|
||||
(
|
||||
"peers",
|
||||
serde_json::to_string(&get_lan_peers()).unwrap_or_default(),
|
||||
),
|
||||
]);
|
||||
return SyncReturn(serde_json::ser::to_string(&data).unwrap_or("".to_owned()));
|
||||
}
|
||||
|
||||
pub fn main_load_ab_sync() -> SyncReturn<String> {
|
||||
return SyncReturn(serde_json::to_string(&config::Ab::load()).unwrap_or_default());
|
||||
}
|
||||
|
||||
pub fn main_load_group_sync() -> SyncReturn<String> {
|
||||
return SyncReturn(serde_json::to_string(&config::Group::load()).unwrap_or_default());
|
||||
}
|
||||
|
||||
pub fn main_load_recent_peers_for_ab(filter: String) -> String {
|
||||
let id_filters = serde_json::from_str::<Vec<String>>(&filter).unwrap_or_default();
|
||||
let id_filters = if id_filters.is_empty() {
|
||||
@ -1072,6 +1119,29 @@ pub fn main_get_main_display() -> SyncReturn<String> {
|
||||
SyncReturn(display_info)
|
||||
}
|
||||
|
||||
pub fn main_get_displays() -> SyncReturn<String> {
|
||||
#[cfg(target_os = "ios")]
|
||||
let display_info = "".to_owned();
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
let mut display_info = "".to_owned();
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
if let Ok(displays) = crate::display_service::try_get_displays() {
|
||||
let displays = displays
|
||||
.iter()
|
||||
.map(|d| {
|
||||
HashMap::from([
|
||||
("x", d.origin().0),
|
||||
("y", d.origin().1),
|
||||
("w", d.width() as i32),
|
||||
("h", d.height() as i32),
|
||||
])
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
display_info = serde_json::to_string(&displays).unwrap_or_default();
|
||||
}
|
||||
SyncReturn(display_info)
|
||||
}
|
||||
|
||||
pub fn session_add_port_forward(
|
||||
session_id: SessionID,
|
||||
local_port: i32,
|
||||
@ -1334,6 +1404,12 @@ pub fn session_on_waiting_for_image_dialog_show(session_id: SessionID) {
|
||||
super::flutter::session_on_waiting_for_image_dialog_show(session_id);
|
||||
}
|
||||
|
||||
pub fn session_toggle_virtual_display(session_id: SessionID, index: i32, on: bool) {
|
||||
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
|
||||
session.toggle_virtual_display(index, on);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main_set_home_dir(_home: String) {
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
{
|
||||
@ -1518,7 +1594,7 @@ pub fn main_is_installed() -> SyncReturn<bool> {
|
||||
|
||||
pub fn main_start_grab_keyboard() -> SyncReturn<bool> {
|
||||
#[cfg(target_os = "linux")]
|
||||
if !*crate::common::IS_X11 {
|
||||
if !crate::platform::linux::is_x11() {
|
||||
return SyncReturn(false);
|
||||
}
|
||||
crate::keyboard::client::start_grab_loop();
|
||||
@ -1869,6 +1945,17 @@ pub fn is_support_multi_ui_session(version: String) -> SyncReturn<bool> {
|
||||
SyncReturn(crate::common::is_support_multi_ui_session(&version))
|
||||
}
|
||||
|
||||
pub fn is_selinux_enforcing() -> SyncReturn<bool> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
SyncReturn(crate::platform::linux::is_selinux_enforcing())
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
SyncReturn(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub mod server_side {
|
||||
use hbb_common::{config, log};
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", "切换到主显示器,因为提权后,不支持多显示器画面。"),
|
||||
("Open in new window", "在新的窗口中打开"),
|
||||
("Show displays as individual windows", "在单个窗口中打开显示器"),
|
||||
("Use all my displays for the remote session", "将我的所有显示器用于远程会话"),
|
||||
("selinux_tip", "SELinux 处于启用状态,RustDesk 可能无法作为被控正常运行。"),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", "虚拟显示器"),
|
||||
("Plug out all", "拔出所有"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -555,14 +555,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Check for software update on startup", "Kontrola aktualizace softwaru při spuštění"),
|
||||
("upgrade_rustdesk_server_pro_to_{}_tip", "Aktualizujte prosím RustDesk Server Pro na verzi {} nebo novější!"),
|
||||
("pull_group_failed_tip", "Nepodařilo se obnovit skupinu"),
|
||||
("Filter by intersection", ""),
|
||||
("Remove wallpaper during incoming sessions", ""),
|
||||
("Test", ""),
|
||||
("switch_display_elevated_connections_tip", ""),
|
||||
("display_is_plugged_out_msg", ""),
|
||||
("No displays", ""),
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Filter by intersection", "Filtrovat podle průsečíku"),
|
||||
("Remove wallpaper during incoming sessions", "Odstranit tapetu během příchozích relací"),
|
||||
("Test", "Test"),
|
||||
("switch_display_elevated_connections_tip", "Přepnutí na jinou než primární obrazovku není podporováno ve zvýšeném režimu, pokud existuje více připojení. Pokud chcete ovládat více obrazovek, zkuste to po instalaci znovu."),
|
||||
("display_is_plugged_out_msg", "Obrazovka je odpojena, přepněte na první obrazovku."),
|
||||
("No displays", "Žádné obrazovky"),
|
||||
("elevated_switch_display_msg", "Přepnout na primární obrazovku, protože více obrazovek není podporováno ve zvýšeném režimu."),
|
||||
("Open in new window", "Otevřít v novém okně"),
|
||||
("Show displays as individual windows", "Zobrazit obrazovky jako jednotlivá okna"),
|
||||
("Use all my displays for the remote session", "Použít všechny mé obrazovky pro vzdálenou relaci"),
|
||||
("selinux_tip", "Na vašem zařízení je povolen SELinux, což může bránit správnému běhu RustDesku jako řízené strany."),
|
||||
("Change view", "Změnit pohled"),
|
||||
("Big tiles", "Velké dlaždice"),
|
||||
("Small tiles", "Malé dlaždice"),
|
||||
("List", "Seznam"),
|
||||
("Virtual display", "Virtuální obrazovka"),
|
||||
("Plug out all", "Odpojit všechny"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -440,7 +440,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Strong", "Stark"),
|
||||
("Switch Sides", "Seiten wechseln"),
|
||||
("Please confirm if you want to share your desktop?", "Bitte bestätigen Sie, wenn Sie Ihren Desktop freigeben möchten."),
|
||||
("Display", "Anzeige"),
|
||||
("Display", "Bildschirm"),
|
||||
("Default View Style", "Standard-Ansichtsstil"),
|
||||
("Default Scroll Style", "Standard-Scroll-Stil"),
|
||||
("Default Image Quality", "Standard-Bildqualität"),
|
||||
@ -476,7 +476,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Empty Password", "Leeres Passwort"),
|
||||
("Me", "Ich"),
|
||||
("identical_file_tip", "Diese Datei ist identisch mit der Datei der Gegenstelle."),
|
||||
("show_monitors_tip", "Monitore in der Symbolleiste anzeigen"),
|
||||
("show_monitors_tip", "Bildschirme in der Symbolleiste anzeigen"),
|
||||
("View Mode", "Ansichtsmodus"),
|
||||
("login_linux_tip", "Sie müssen sich an einem entfernten Linux-Konto anmelden, um eine X-Desktop-Sitzung zu eröffnen."),
|
||||
("verify_rustdesk_password_tip", "RustDesk-Passwort bestätigen"),
|
||||
@ -558,11 +558,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Filter by intersection", "Nach Schnittmenge filtern"),
|
||||
("Remove wallpaper during incoming sessions", "Hintergrundbild während eingehender Sitzungen entfernen"),
|
||||
("Test", "Test"),
|
||||
("switch_display_elevated_connections_tip", "Das Umschalten auf eine nicht primäre Anzeige wird mit erhöhten Rechten nicht unterstützt, wenn mehrere Verbindungen bestehen. Bitte versuchen Sie es nach der Installation erneut, wenn Sie mehrere Anzeigen steuern möchten."),
|
||||
("display_is_plugged_out_msg", "Das Anzeigegerät ist nicht angeschlossen, schalten Sie auf das erste Anzeigegerät um."),
|
||||
("No displays", "Keine Anzeigegeräte"),
|
||||
("elevated_switch_display_msg", "Wechseln Sie zur primären Anzeige, da die Mehrfachanzeige im erweiterten Modus nicht unterstützt wird."),
|
||||
("switch_display_elevated_connections_tip", "Das Umschalten auf einen sekundären Bildschirm wird mit erhöhten Rechten nicht unterstützt, wenn mehrere Verbindungen bestehen. Bitte versuchen Sie es nach der Installation erneut, wenn Sie mehrere Bildschirme steuern möchten."),
|
||||
("display_is_plugged_out_msg", "Der Bildschirm ist nicht angeschlossen, schalten Sie auf den ersten Bildschirm um."),
|
||||
("No displays", "Keine Bildschirme"),
|
||||
("elevated_switch_display_msg", "Wechseln Sie zum primären Bildschirm, da mehrere Bildschirme im erweiterten Modus nicht unterstützt werden."),
|
||||
("Open in new window", "In einem neuen Fenster öffnen"),
|
||||
("Show displays as individual windows", "Anzeigen als einzelne Fenster darstellen"),
|
||||
("Show displays as individual windows", "Jeden Bildschirm in einem eigenen Fenster anzeigen"),
|
||||
("Use all my displays for the remote session", "Alle meine Bildschirme für die Fernsitzung verwenden"),
|
||||
("selinux_tip", "SELinux ist auf Ihrem Gerät aktiviert, was dazu führen kann, dass RustDesk als kontrollierte Seite nicht richtig läuft."),
|
||||
("Change view", "Ansicht ändern"),
|
||||
("Big tiles", "Große Kacheln"),
|
||||
("Small tiles", "Kleine Kacheln"),
|
||||
("List", "Liste"),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -223,7 +223,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("pull_group_failed_tip", "Failed to refresh group"),
|
||||
("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"),
|
||||
("switch_display_elevated_connections_tip", "Switching to non-primary display is not supported in the elevated mode when there are multiple connections. Please try again after installation if you want to control multiple displays."),
|
||||
("display_is_plugged_out_msg", "The diplay is plugged out, switch to the first display."),
|
||||
("elevated_switch_display_msg", "Switch to the primary display because multiple display is not supported in elevated mode."),
|
||||
("display_is_plugged_out_msg", "The display is plugged out, switch to the first display."),
|
||||
("elevated_switch_display_msg", "Switch to the primary display because multiple displays are not supported in elevated mode."),
|
||||
("selinux_tip", "SELinux is enabled on your device, which may prevent RustDesk from running properly as controlled side."),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -555,14 +555,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Check for software update on startup", "Comprobar actualización al iniciar"),
|
||||
("upgrade_rustdesk_server_pro_to_{}_tip", "¡Por favor, actualiza RustDesk Server Pro a la versión {} o superior"),
|
||||
("pull_group_failed_tip", "No se ha podido refrescar el grupo"),
|
||||
("Filter by intersection", ""),
|
||||
("Remove wallpaper during incoming sessions", ""),
|
||||
("Test", ""),
|
||||
("switch_display_elevated_connections_tip", ""),
|
||||
("display_is_plugged_out_msg", ""),
|
||||
("No displays", ""),
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Filter by intersection", "Filtrar por intersección"),
|
||||
("Remove wallpaper during incoming sessions", "Quitar el fonde de pantalla durante sesiones entrantes"),
|
||||
("Test", "Probar"),
|
||||
("switch_display_elevated_connections_tip", "Cambiar a una pantalla no principal no está soportado en el modo elevado cuando hay múltiples conexiones. Por favor, inténtalo de nuevo tras la instalación si quieres controlar múltiples pantallas."),
|
||||
("display_is_plugged_out_msg", "La pantalla está desconectada, cambia a la principal."),
|
||||
("No displays", "No hay pantallas"),
|
||||
("elevated_switch_display_msg", "Cambiar a la pantalla principal porque mútliples pantallas no están soportadas en modo elevado."),
|
||||
("Open in new window", "Abrir en una nueva ventana"),
|
||||
("Show displays as individual windows", "Mostrar pantallas como ventanas individuales"),
|
||||
("Use all my displays for the remote session", "Usar todas mis pantallas para la sesión remota"),
|
||||
("selinux_tip", "SELinux está activado en tu dispositivo, lo que puede hacer que RustDesk no se ejecute correctamente como lado controlado."),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("selinux_tip", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Home", ""),
|
||||
("Audio Input", "Input Audio"),
|
||||
("Enhancements", "Peningkatan"),
|
||||
("Hardware Codec", "Codec Perangkat Keras"),
|
||||
("Hardware Codec", "Kodek Perangkat Keras"),
|
||||
("Adaptive bitrate", "Kecepatan Bitrate Adaptif"),
|
||||
("ID Server", "Server ID"),
|
||||
("Relay Server", "Server Relay"),
|
||||
@ -352,7 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Dark", "Gelap"),
|
||||
("Light", "Terang"),
|
||||
("Follow System", "Ikuti Sistem"),
|
||||
("Enable hardware codec", "Aktifkan codec perangkat keras"),
|
||||
("Enable hardware codec", "Aktifkan kodek perangkat keras"),
|
||||
("Unlock Security Settings", "Buka Keamanan Pengaturan"),
|
||||
("Enable Audio", "Aktifkan Audio"),
|
||||
("Unlock Network Settings", "Buka Keamanan Pengaturan Jaringan"),
|
||||
@ -369,7 +369,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Unpin Toolbar", "Batal sematkan Toolbar"),
|
||||
("Recording", "Perekaman"),
|
||||
("Directory", "Direktori"),
|
||||
("Automatically record incoming sessions", "Secara otomatis merekam sesi masuk"),
|
||||
("Automatically record incoming sessions", "Otomatis merekam sesi masuk"),
|
||||
("Change", "Ubah"),
|
||||
("Start session recording", "Mulai sesi perekaman"),
|
||||
("Stop session recording", "Hentikan sesi perekaman"),
|
||||
@ -444,7 +444,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Default View Style", "Gaya Tampilan Default"),
|
||||
("Default Scroll Style", "Gaya Scroll Default"),
|
||||
("Default Image Quality", "Kualitas Gambar Default"),
|
||||
("Default Codec", "Codec default"),
|
||||
("Default Codec", "Kodek default"),
|
||||
("Bitrate", "Bitrate"),
|
||||
("FPS", "FPS"),
|
||||
("Auto", "Otomatis"),
|
||||
@ -454,9 +454,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Stop voice call", "Hentikan panggilan suara"),
|
||||
("relay_hint_tip", "Tidak memungkinkan untuk terhubung secara langsung; anda bisa mencoba terhubung via relay. Selain itu, jika ingin menggunakan relay pada percobaan pertama, silahkan tambah akhiran \"/r\" pada ID atau pilih \"Selalu terhubung via relay\" di pilihan sesi terbaru."),
|
||||
("Reconnect", "Menyambungkan ulang"),
|
||||
("Codec", "Codec"),
|
||||
("Codec", "Kodek"),
|
||||
("Resolution", "Resolusi"),
|
||||
("No transfers in progress", "Tidak ada transfer data yang sedang berlangsung"),
|
||||
("No transfers in progress", "Tidak ada proses transfer data"),
|
||||
("Set one-time password length", "Atur panjang kata sandi sekali pakai"),
|
||||
("install_cert_tip", "Install sertifikat RustDesk"),
|
||||
("confirm_install_cert_tip", "Ini adalah sertifikat pengujian RustDesk, yang dapat dipercaya. Sertifikat ini akan digunakan untuk menginstal driver RustDesk saat diperlukan"),
|
||||
@ -541,7 +541,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("HSV Color", "Warna HSV"),
|
||||
("Installation Successful!", "Instalasi berhasil!"),
|
||||
("Installation failed!", "Instalasi gagal!"),
|
||||
("Reverse mouse wheel", "Balikkan arah scroll mouse!"),
|
||||
("Reverse mouse wheel", "Balikkan arah scroll mouse"),
|
||||
("{} sessions", "sesi {}"),
|
||||
("scam_title", "Kemungkinan Anda Sedang DITIPU!"),
|
||||
("scam_text1", "Jika Anda sedang berbicara di telepon dengan seseorang yang TIDAK dikenal dan mereka meminta anda untuk menggunakan RustDesk, jangan lanjutkan dan segera tutup panggilan."),
|
||||
@ -563,6 +563,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("No displays", "Tidak ada tampilan"),
|
||||
("elevated_switch_display_msg", "Pindah ke tampilan utama, pada mode elevasi, pengggunaan lebih dari satu layar tidak diizinkan"),
|
||||
("Open in new window", "Buka di jendela baru"),
|
||||
("Show displays as individual windows", "Tampilkan layar sebagai jendela terpisah"),
|
||||
("Show displays as individual windows", "Tampilkan dengan jendela terpisah"),
|
||||
("Use all my displays for the remote session", "Gunakan semua layar untuk sesi remote"),
|
||||
("selinux_tip", ""),
|
||||
("Change view", "Sesuaikan tampilan"),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", "Tampilan virtual"),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -365,7 +365,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Audio Input Device", "Dispositivo ingresso audio"),
|
||||
("Use IP Whitelisting", "Usa elenco IP autorizzati"),
|
||||
("Network", "Rete"),
|
||||
("Enable RDP", "Abilita RDP"),
|
||||
("Pin Toolbar", "Blocca barra strumenti"),
|
||||
("Unpin Toolbar", "Sblocca barra strumenti"),
|
||||
("Recording", "Registrazione"),
|
||||
@ -565,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", "Passo allo schermo principale perché in modalità elevata non sono supportati più schermi."),
|
||||
("Open in new window", "Apri in una nuova finestra"),
|
||||
("Show displays as individual windows", "Visualizza schermi come finestre individuali"),
|
||||
("Use all my displays for the remote session", "Usa tutti gli schermi per la sessione remota"),
|
||||
("selinux_tip", "In questo dispositivo è abilitato SELinux, che potrebbe impedire il corretto funzionamento di RustDesk come lato controllato."),
|
||||
("Change view", "Modifica vista"),
|
||||
("Big tiles", "Icone grandi"),
|
||||
("Small tiles", "Icone piccole"),
|
||||
("List", "Elenco"),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", "Pārslēdzieties uz primāro displeju, jo paaugstinātajā režīmā netiek atbalstīti vairāki displeji."),
|
||||
("Open in new window", "Atvērt jaunā logā"),
|
||||
("Show displays as individual windows", "Rādīt displejus kā atsevišķus logus"),
|
||||
("Use all my displays for the remote session", "Izmantot visus manus displejus attālajai sesijai"),
|
||||
("selinux_tip", "Jūsu ierīcē ir iespējots SELinux, kas var neļaut RustDesk pareizi darboties kā kontrolētajai pusei."),
|
||||
("Change view", "Mainīt skatu"),
|
||||
("Big tiles", "Lielas flīzes"),
|
||||
("Small tiles", "Mazas flīzes"),
|
||||
("List", "Saraksts"),
|
||||
("Virtual display", "Virtuālais displejs"),
|
||||
("Plug out all", "Atvienot visu"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -555,14 +555,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Check for software update on startup", "Checken voor updates bij opstarten"),
|
||||
("upgrade_rustdesk_server_pro_to_{}_tip", "Upgrade RustDesk Server Pro naar versie {} of nieuwer!"),
|
||||
("pull_group_failed_tip", "Vernieuwen van groep mislukt"),
|
||||
("Filter by intersection", ""),
|
||||
("Remove wallpaper during incoming sessions", ""),
|
||||
("Test", ""),
|
||||
("switch_display_elevated_connections_tip", ""),
|
||||
("display_is_plugged_out_msg", ""),
|
||||
("No displays", ""),
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Filter by intersection", "Filter op kruising"),
|
||||
("Remove wallpaper during incoming sessions", "Achtergrond verwijderen tijdens inkomende sessies"),
|
||||
("Test", "Test"),
|
||||
("switch_display_elevated_connections_tip", "Overschakelen naar een niet-hoofdbeeldscherm wordt niet ondersteund in de verhoogde modus wanneer er meerdere verbindingen zijn. Probeer het opnieuw na de installatie als je meerdere schermen wilt beheren."),
|
||||
("display_is_plugged_out_msg", "Beeldscherm is uitgeschakeld, schakel over naar het primaire beeldscherm."),
|
||||
("No displays", "Geen beeldschermen"),
|
||||
("elevated_switch_display_msg", "Schakel over naar het primaire beeldscherm, aangezien meerdere beeldschermen niet worden ondersteund in de modus met verhoogde rechten."),
|
||||
("Open in new window", "Open in een nieuw venster"),
|
||||
("Show displays as individual windows", "Beeldschermen weergeven als afzonderlijke vensters"),
|
||||
("Use all my displays for the remote session", "Gebruik al mijn beeldschermen voor de externe sessie"),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", "Przełącz się na ekran główny, ponieważ wyświetlanie kilku ekranów nie jest obsługiwane przy podniesionych uprawnieniach."),
|
||||
("Open in new window", "Otwórz w nowym oknie"),
|
||||
("Show displays as individual windows", "Pokaż ekrany w osobnych oknach"),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", "Переключитесь на основной дисплей, поскольку в режиме повышенных прав несколько дисплеев не поддерживаются."),
|
||||
("Open in new window", "Открыть в новом окне"),
|
||||
("Show displays as individual windows", "Показывать дисплеи в отдельных окнах"),
|
||||
("Use all my displays for the remote session", "Использовать все мои дисплеи для удалённого сеанса"),
|
||||
("selinux_tip", "На вашем устройстве включён SELinux, что может помешать правильной работе RustDesk на контролируемой стороне."),
|
||||
("Change view", "Вид"),
|
||||
("Big tiles", "Большие значки"),
|
||||
("Small tiles", "Маленькие значки"),
|
||||
("List", "Список"),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
112
src/lang/ua.rs
112
src/lang/ua.rs
@ -138,7 +138,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Please try later", "Будь ласка, спробуйте пізніше"),
|
||||
("Remote desktop is offline", "Віддалена стільниця не в мережі"),
|
||||
("Key mismatch", "Невідповідність ключів"),
|
||||
("Timeout", "Тайм-аут"),
|
||||
("Timeout", "Час очікування"),
|
||||
("Failed to connect to relay server", "Не вдалося підключитися до сервера реле"),
|
||||
("Failed to connect via rendezvous server", "Не вдалося підключитися через проміжний сервер"),
|
||||
("Failed to connect via relay server", "Не вдалося підключитися через сервер реле"),
|
||||
@ -223,7 +223,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Verification code", "Код підтвердження"),
|
||||
("verification_tip", "Виявлено новий пристрій, код підтвердження надіслано на зареєстровану email-адресу, введіть код підтвердження для продовження авторизації."),
|
||||
("Logout", "Вийти"),
|
||||
("Tags", "Ключові слова"),
|
||||
("Tags", "Теги"),
|
||||
("Search ID", "Пошук за ID"),
|
||||
("whitelist_sep", "Розділені комою, крапкою з комою, пробілом або новим рядком"),
|
||||
("Add ID", "Додати ID"),
|
||||
@ -231,7 +231,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Unselect all tags", "Скасувати вибір усіх тегів"),
|
||||
("Network error", "Помилка мережі"),
|
||||
("Username missed", "Імʼя користувача відсутнє"),
|
||||
("Password missed", "Забули пароль"),
|
||||
("Password missed", "Пароль відсутній"),
|
||||
("Wrong credentials", "Неправильні дані"),
|
||||
("The verification code is incorrect or has expired", "Код підтвердження некоректний або протермінований"),
|
||||
("Edit Tag", "Редагувати тег"),
|
||||
@ -269,7 +269,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Reset canvas", "Відновлення полотна"),
|
||||
("No permission of file transfer", "Немає дозволу на передачу файлів"),
|
||||
("Note", "Примітка"),
|
||||
("Connection", "Зʼєднання"),
|
||||
("Connection", "Підключення"),
|
||||
("Share Screen", "Поділитися екраном"),
|
||||
("Chat", "Чат"),
|
||||
("Total", "Всього"),
|
||||
@ -516,53 +516,61 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Exit", "Вийти"),
|
||||
("Open", "Відкрити"),
|
||||
("logout_tip", "Ви впевнені, що хочете вилогуватися?"),
|
||||
("Service", ""),
|
||||
("Start", ""),
|
||||
("Stop", ""),
|
||||
("exceed_max_devices", ""),
|
||||
("Sync with recent sessions", ""),
|
||||
("Sort tags", ""),
|
||||
("Open connection in new tab", ""),
|
||||
("Move tab to new window", ""),
|
||||
("Can not be empty", ""),
|
||||
("Already exists", ""),
|
||||
("Change Password", ""),
|
||||
("Refresh Password", ""),
|
||||
("ID", ""),
|
||||
("Grid View", ""),
|
||||
("List View", ""),
|
||||
("Select", ""),
|
||||
("Toggle Tags", ""),
|
||||
("pull_ab_failed_tip", ""),
|
||||
("push_ab_failed_tip", ""),
|
||||
("synced_peer_readded_tip", ""),
|
||||
("Change Color", ""),
|
||||
("Primary Color", ""),
|
||||
("HSV Color", ""),
|
||||
("Installation Successful!", ""),
|
||||
("Installation failed!", ""),
|
||||
("Reverse mouse wheel", ""),
|
||||
("{} sessions", ""),
|
||||
("scam_title", ""),
|
||||
("scam_text1", ""),
|
||||
("scam_text2", ""),
|
||||
("Don't show again", ""),
|
||||
("I Agree", ""),
|
||||
("Decline", ""),
|
||||
("Timeout in minutes", ""),
|
||||
("auto_disconnect_option_tip", ""),
|
||||
("Connection failed due to inactivity", ""),
|
||||
("Check for software update on startup", ""),
|
||||
("upgrade_rustdesk_server_pro_to_{}_tip", ""),
|
||||
("pull_group_failed_tip", ""),
|
||||
("Filter by intersection", ""),
|
||||
("Remove wallpaper during incoming sessions", ""),
|
||||
("Test", ""),
|
||||
("switch_display_elevated_connections_tip", ""),
|
||||
("display_is_plugged_out_msg", ""),
|
||||
("No displays", ""),
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Service", "Служба"),
|
||||
("Start", "Запустити"),
|
||||
("Stop", "Зупинити"),
|
||||
("exceed_max_devices", "У вас максимальна кількість керованих пристроїв."),
|
||||
("Sync with recent sessions", "Синхронізація з нещодавніми сеансами"),
|
||||
("Sort tags", "Сортувати теги"),
|
||||
("Open connection in new tab", "Відкрити підключення в новій вкладці"),
|
||||
("Move tab to new window", "Перемістити вкладку до нового вікна"),
|
||||
("Can not be empty", "Не може бути порожнім"),
|
||||
("Already exists", "Вже існує"),
|
||||
("Change Password", "Змінити пароль"),
|
||||
("Refresh Password", "Оновити пароль"),
|
||||
("ID", "ID"),
|
||||
("Grid View", "Перегляд ґраткою"),
|
||||
("List View", "Перегляд списком"),
|
||||
("Select", "Вибрати"),
|
||||
("Toggle Tags", "Видимість тегів"),
|
||||
("pull_ab_failed_tip", "Не вдалося оновити адресну книгу"),
|
||||
("push_ab_failed_tip", "Не вдалося синхронізувати адресну книгу"),
|
||||
("synced_peer_readded_tip", "Пристрої з нещодавніх сеансів будуть синхронізовані з адресною книгою"),
|
||||
("Change Color", "Змінити колір"),
|
||||
("Primary Color", "Основний колір"),
|
||||
("HSV Color", "Колір HSV"),
|
||||
("Installation Successful!", "Успішне встановлення!"),
|
||||
("Installation failed!", "Невдале встановлення!"),
|
||||
("Reverse mouse wheel", "Зворотній напрям прокрутки"),
|
||||
("{} sessions", "{} сеансів"),
|
||||
("scam_title", "Вас можуть ОБМАНУТИ!"),
|
||||
("scam_text1", "Якщо ви розмовляєте по телефону з кимось, кого НЕ ЗНАЄТЕ чи кому НЕ ДОВІРЯЄТЕ, і ця особа хоче, щоб ви використали RustDesk та запустили службу, не робіть цього та негайно завершіть дзвінок."),
|
||||
("scam_text2", "Ймовірно, ви маєте справу з шахраєм, що намагається викрасти ваші гроші чи особисті дані."),
|
||||
("Don't show again", "Не показувати знову"),
|
||||
("I Agree", "Я погоджуюсь"),
|
||||
("Decline", "Я не погоджуюсь"),
|
||||
("Timeout in minutes", "Час очікування в хвилинах"),
|
||||
("auto_disconnect_option_tip", "Автоматично завершувати вхідні сеанси в разі неактивності користувача"),
|
||||
("Connection failed due to inactivity", "Автоматично відключено через неактивність"),
|
||||
("Check for software update on startup", "Перевіряти оновлення під час запуску"),
|
||||
("upgrade_rustdesk_server_pro_to_{}_tip", "Будь ласка, оновіть RustDesk Server Pro до версії {} чи новіше!"),
|
||||
("pull_group_failed_tip", "Не вдалося оновити групу"),
|
||||
("Filter by intersection", "Фільтр за збігом"),
|
||||
("Remove wallpaper during incoming sessions", "Прибирати шпалеру під час вхідних сеансів"),
|
||||
("Test", "Тест"),
|
||||
("switch_display_elevated_connections_tip", "В режимі розширених прав, коли є декілька підключень, не підтримується перемикання на неосновний дисплей. Якщо вам потрібен контроль декількох дисплеїв, будь ласка, спробуйте ще раз після встановлення"),
|
||||
("display_is_plugged_out_msg", "Дисплей відключено, перемкніться на перший дисплей"),
|
||||
("No displays", "Відсутні дисплеї"),
|
||||
("elevated_switch_display_msg", "Перемкніться на основний дисплей, оскільки в режимі розширених прав одночасне використання декілька дисплеїв не підтримуються."),
|
||||
("Open in new window", "Відкрити в новому вікні"),
|
||||
("Show displays as individual windows", "Відображати дисплеї в якості окремих вікон"),
|
||||
("Use all my displays for the remote session", "Використовувати всі мої дисплеї для віддаленого сеансу"),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -564,5 +564,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("elevated_switch_display_msg", ""),
|
||||
("Open in new window", ""),
|
||||
("Show displays as individual windows", ""),
|
||||
("Use all my displays for the remote session", ""),
|
||||
("selinux_tip", ""),
|
||||
("Change view", ""),
|
||||
("Big tiles", ""),
|
||||
("Small tiles", ""),
|
||||
("List", ""),
|
||||
("Virtual display", ""),
|
||||
("Plug out all", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
@ -35,6 +35,10 @@ type Xdo = *const c_void;
|
||||
pub const PA_SAMPLE_RATE: u32 = 48000;
|
||||
static mut UNMODIFIED: bool = true;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref IS_X11: bool = hbb_common::platform::linux::is_x11_or_headless();
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static XDO: RefCell<Xdo> = RefCell::new(unsafe { xdo_new(std::ptr::null()) });
|
||||
static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())});
|
||||
@ -1360,3 +1364,26 @@ impl Drop for WallPaperRemover {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_x11() -> bool {
|
||||
*IS_X11
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_selinux_enforcing() -> bool {
|
||||
match run_cmds("getenforce") {
|
||||
Ok(output) => output.trim() == "Enforcing",
|
||||
Err(_) => match run_cmds("sestatus") {
|
||||
Ok(output) => {
|
||||
for line in output.lines() {
|
||||
if line.contains("Current mode:") {
|
||||
return line.contains("enforcing");
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
Err(_) => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user