`.
+Ad esempio, se vuoi creare una versione di rilascio ottimizzata, esegui il comando precedentemente indicato seguito da `--release`.
+L'eseguibile generato sarà creato nella cartella destinazione del sistema e può essere eseguito con:
```sh
target/debug/rustdesk
```
-Oppure, se si sta eseguendo un eseguibile di rilascio:
+Oppure, se stai avviando un eseguibile di rilascio:
```sh
target/release/rustdesk
```
-Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altrimenti l'applicazione potrebbe non essere in grado di trovare le risorse richieste. Notare inoltre che altri sottocomandi cargo come `install` o `run` non sono attualmente supportati tramite questo metodo poiché installerebbero o eseguirebbero il programma all'interno del container anziché nell'host.
+Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altrimenti l'applicazione potrebbe non essere in grado di trovare le risorse richieste.
+Nota inoltre che altri sottocomandi cargo come `install` o `run` non sono attualmente supportati tramite questo metodo poiché installerebbero o eseguirebbero il programma all'interno del container anziché nell'host.
## Struttura dei file
-- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs funzioni per il trasferimento file, e altre funzioni utili.
+- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec video, config, wrapper tcp/udp, protobuf, funzioni per il trasferimento file, e altre funzioni utili.
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: cattura dello schermo
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controllo tastiera/mouse specifico della piattaforma
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: servizi audio/appunti/input/video e connessioni di rete
-- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: avviare una connessione peer
+- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: avvio di una connessione peer
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunica con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attende la connessione remota diretta (TCP hole punching) oppure indiretta (relayed)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: codice specifico della piattaforma
-## Screenshots
+## Schermate

diff --git a/docs/README-JP.md b/docs/README-JP.md
index 709d41547..44f811eec 100644
--- a/docs/README-JP.md
+++ b/docs/README-JP.md
@@ -29,11 +29,7 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CON
下記のサーバーは、無料で使用できますが、後々変更されることがあります。これらのサーバーから遠い場合、接続が遅い可能性があります。
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
## 依存関係
diff --git a/docs/README-KR.md b/docs/README-KR.md
index 7c6326cc8..dacb092e7 100644
--- a/docs/README-KR.md
+++ b/docs/README-KR.md
@@ -29,11 +29,7 @@ RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/C
표에 있는 서버는 무료로 사용할 수 있지만 추후 변경될 수도 있습니다. 이 서버에서 멀다면, 네트워크가 느려질 가능성도 있습니다.
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
## 의존관계
diff --git a/docs/README-ML.md b/docs/README-ML.md
index 9f3ed88a3..a73fd7815 100644
--- a/docs/README-ML.md
+++ b/docs/README-ML.md
@@ -24,11 +24,7 @@
നിങ്ങൾ സൗജന്യമായി ഉപയോഗിക്കുന്ന സെർവറുകൾ ചുവടെയുണ്ട്, അത് സമയത്തിനനുസരിച്ച് മാറിയേക്കാം. നിങ്ങൾ ഇവയിലൊന്നിനോട് അടുത്തല്ലെങ്കിൽ, നിങ്ങളുടെ നെറ്റ്വർക്ക് സ്ലോ ആയേക്കാം.
| സ്ഥാനം | കച്ചവടക്കാരൻ | വിവരണം |
| --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
## ഡിപെൻഡൻസികൾ
diff --git a/docs/README-NL.md b/docs/README-NL.md
index 2214adeac..bec83a285 100644
--- a/docs/README-NL.md
+++ b/docs/README-NL.md
@@ -32,11 +32,7 @@ RustDesk verwelkomt bijdragen van iedereen. Zie [`docs/CONTRIBUTING.md`](CONTRIB
Hieronder staan de servers die u gratis gebruikt, ze kunnen in de loop van de tijd veranderen. Als u niet in de buurt van een van deze servers bevindt, kan uw vervinding langzamer zijn.
| Locatie | Aanbieder | Specificaties |
| --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
| Duitsland | Hetzner | 2 vCPU / 4GB RAM |
-| Duitsland | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
| Oekraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dev Container
diff --git a/docs/README-PL.md b/docs/README-PL.md
index f44542581..ba27af04d 100644
--- a/docs/README-PL.md
+++ b/docs/README-PL.md
@@ -34,11 +34,7 @@ RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING-PL.md`](C
Poniżej znajdują się serwery, z których można korzystać za darmo, może się to zmienić z upływem czasu. Jeśli nie znajdujesz się w pobliżu jednego z nich, Twoja prędkość połączenia może być niska.
| Lokalizacja | Dostawca | Specyfikacja |
| --------- | ------------- | ------------------ |
-| Korea Płd. (Seul) | AWS lightsail | 1 vCPU / 0.5GB RAM |
| Niemcy | Hetzner | 2 vCPU / 4GB RAM |
-| Niemcy | Codext | 4 vCPU / 8GB RAM |
-| Finlandia (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
| Ukraina (Kijów) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Konterner Programisty (Dev Container)
diff --git a/docs/README-PTBR.md b/docs/README-PTBR.md
index 7e8a2d2e7..6e6f01fce 100644
--- a/docs/README-PTBR.md
+++ b/docs/README-PTBR.md
@@ -25,11 +25,7 @@ Abaixo estão os servidores que você está utilizando de graça, ele pode mudar
| Localização | Fornecedor | Especificações |
| ----------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
## Dependências
diff --git a/docs/README-RU.md b/docs/README-RU.md
index 282617ace..01710f084 100644
--- a/docs/README-RU.md
+++ b/docs/README-RU.md
@@ -33,13 +33,7 @@ RustDesk приветствует вклад каждого. Ознакомьт
Ниже приведены бесплатные публичные сервера, используемые по умолчанию. Имейте ввиду, они могут меняться со временем. Также стоит отметить, что скорость работы сети зависит от вашего местоположения и расстояния до серверов. Подключение происходит к ближайшему доступному.
| Расположение | Поставщик | Технические характеристики |
| --------- | ------------- | ------------------ |
-| Сеул | AWS lightsail | 1 vCPU / 0.5GB RAM |
-| Сингапур | Vultr | 1 vCPU / 1GB RAM |
-| Даллас | Vultr | 1 vCPU / 1GB RAM |
| Германия | Hetzner | 2 vCPU / 4GB RAM |
-| Германия | Codext | 4 vCPU / 8GB RAM |
-| Финляндия (Хельсинки) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| США (Эшберн) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
## Зависимости
diff --git a/docs/README-TR.md b/docs/README-TR.md
new file mode 100644
index 000000000..590ead0df
--- /dev/null
+++ b/docs/README-TR.md
@@ -0,0 +1,223 @@
+
+
+
+ Sunucular •
+ Derleme •
+ Docker ile Derleme •
+ Dosya Yapısı •
+ Ekran Görüntüleri
+ [Українська ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Deutsch ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ] | [Dansk ] | [Ελληνικά ]
+ README, RustDesk UI ve RustDesk Belge 'sini ana dilinize çevirmemiz için yardımınıza ihtiyacımız var
+
+
+Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+
+[](https://ko-fi.com/I2I04VU09)
+
+Başka bir uzak masaüstü yazılımı daha, Rust dilinde yazılmış. Hemen kullanıma hazır, hiçbir yapılandırma gerektirmez. Verilerinizin tam kontrolünü elinizde tutarsınız ve güvenlikle ilgili endişeleriniz olmaz. Kendi buluş/iletme sunucumuzu kullanabilirsiniz, [kendi sunucunuzu kurabilirsiniz](https://rustdesk.com/server) veya [kendi buluş/iletme sunucunuzu yazabilirsiniz](https://github.com/rustdesk/rustdesk-server-demo).
+
+
+
+RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs/CONTRIBUTING-TR.md) belgesine göz atın.
+
+[**SSS**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
+
+[**BİNARİ İNDİR**](https://github.com/rustdesk/rustdesk/releases)
+
+[**NİGHTLY DERLEME**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
+
+[ ](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+
+## Ücretsiz Genel Sunucular
+
+Aşağıda ücretsiz olarak kullandığınız sunucular listelenmiştir, zaman içinde değişebilirler. Eğer bunlardan birine yakın değilseniz, ağınız yavaş olabilir.
+| Konum | Sağlayıcı | Özellikler |
+| --------- | ------------- | ------------------ |
+| Almanya | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
+| Almanya | [Codext](https://codext.de) | 4 vCPU / 8 GB RAM |
+| Ukrayna (Kiev) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
+
+## Geliştirici Konteyneri
+
+[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
+
+Eğer zaten VS Code ve Docker kurulu ise yukarıdaki rozete tıklayarak başlayabilirsiniz. Tıklamak, VS Code'un gerektiğinde Dev Konteyner eklentisini otomatik olarak yüklemesine, kaynak kodunu bir konteyner hacmine klonlamasına ve kullanım için bir geliştirici konteyneri başlatmasına neden olur.
+
+Daha fazla bilgi için [DEVCONTAINER.md](docs/DEVCONTAINER-TR.md) belgesine bakabilirsiniz.
+
+## Bağımlılıklar
+
+Masaüstü sürümleri GUI için
+
+ [Sciter](https://sciter.com/) veya Flutter kullanır, bu kılavuz sadece Sciter içindir.
+
+Lütfen Sciter dinamik kütüphanesini kendiniz indirin.
+
+[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
+[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
+[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
+
+## Temel Derleme Adımları
+
+- Rust geliştirme ortamınızı ve C++ derleme ortamınızı hazırlayın.
+
+- [vcpkg](https://github.com/microsoft/vcpkg) yükleyin ve `VCPKG_ROOT` çevresel değişkenini doğru bir şekilde ayarlayın.
+
+ - 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
+
+- `cargo run` komutunu çalıştırın.
+
+## [Derleme](https://rustdesk.com/docs/en/dev/build/)
+
+## Linux Üzerinde Derleme Nasıl Yapılır
+
+### Ubuntu 18 (Debian 10)
+
+```sh
+sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
+ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
+ libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
+```
+
+### openSUSE Tumbleweed
+
+```sh
+sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
+```
+
+### Fedora 28 (CentOS 8)
+
+```sh
+sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
+```
+
+### Arch (Manjaro)
+
+```sh
+sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
+```
+
+### vcpkg'yi Yükleyin
+
+```sh
+git clone https://github.com/microsoft/vcpkg
+cd vcpkg
+git checkout 2023.04.15
+cd ..
+vcpkg/bootstrap-vcpkg.sh
+export VCPKG_ROOT=$HOME/vcpkg
+vcpkg/vcpkg install libvpx libyuv opus aom
+```
+
+### libvpx'i Düzeltin (Fedora için)
+
+```sh
+cd vcpkg/buildtrees/libvpx/src
+cd *
+./configure
+sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
+sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
+make
+cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
+cd
+```
+
+### Derleme
+
+```sh
+curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+source $HOME/.cargo/env
+git clone https://github.com/rustdesk/rustdesk
+cd rustdesk
+mkdir -p target/debug
+wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
+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:
+
+```sh
+git clone https://github.com/rustdesk/rustdesk
+cd rustdesk
+docker build -t "rustdesk-builder" .
+```
+
+Ardından, uygulamayı derlemek için her seferinde aşağıdaki komutu çalıştırın:
+
+```sh
+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
+```
+
+İlk derleme, bağımlılıklar önbelleğe alınmadan önce daha uzun sürebilir, sonraki derlemeler daha hızlı olacaktır. Ayrıca, derleme komutuna isteğe bağlı argümanlar belirtmeniz gerekiyorsa, bunu
+
+ komutun sonunda `<İSTEĞE BAĞLI-ARGÜMANLAR>` pozisyonunda yapabilirsiniz. Örneğin, optimize edilmiş bir sürümü derlemek isterseniz, yukarıdaki komutu çalıştırdıktan sonra `--release` ekleyebilirsiniz. Oluşan yürütülebilir dosya sisteminizdeki hedef klasöründe bulunacak ve şu komutla çalıştırılabilir:
+
+```sh
+target/debug/rustdesk
+```
+
+Veya, yayın yürütülebilir dosyası çalıştırılıyorsa:
+
+```sh
+target/release/rustdesk
+```
+
+Lütfen bu komutları RustDesk deposunun kökünden çalıştırdığınızdan emin olun, aksi takdirde uygulama gereken kaynakları bulamayabilir. Ayrıca, `install` veya `run` gibi diğer cargo altkomutları şu anda bu yöntem aracılığıyla desteklenmemektedir, çünkü bunlar programı konteyner içinde kurar veya çalıştırır ve ana makinede değil.
+
+## Dosya Yapısı
+
+- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video kodlayıcı, yapılandırma, tcp/udp sarmalayıcı, protobuf, dosya transferi için fs işlevleri ve diğer bazı yardımcı işlevler
+- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ekran yakalama
+- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platforma özgü klavye/fare kontrolü
+- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
+- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ses/pasta/klavye/video hizmetleri ve ağ bağlantıları
+- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bir eş bağlantısı başlatır
+- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server) ile iletişim kurar, uzak doğrudan (TCP delik vurma) veya iletme bağlantısını bekler
+- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platforma özgü kod
+- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: mobil için Flutter kodu
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter web istemcisi için JavaScript
+
+## Ekran Görüntüleri
+
+
+
+
+
+
+
+
+```
diff --git a/docs/README-UA.md b/docs/README-UA.md
index c6c8e66f6..01914cfc2 100644
--- a/docs/README-UA.md
+++ b/docs/README-UA.md
@@ -34,13 +34,7 @@ RustDesk вітає внесок кожного. Дивіться [`docs/CONTRIB
Нижче наведені сервери, для безкоштовного використання, вони можуть змінюватися з часом. Якщо ви не перебуваєте поруч з одним із них, ваша мережа може працювати повільно.
| Місцезнаходження | Постачальник | Технічні характеристики |
| --------- | ------------- | ------------------ |
-| Південна Корея (Сеул) | AWS lightsail | 1 vCPU / 0.5GB RAM |
-| Сінгапур | Vultr | 1 vCPU / 1GB RAM |
-| США (Даллас) | Vultr | 1 vCPU / 1GB RAM
| Німеччина | Hetzner | 2 VCPU / 4GB RAM |
-| Німеччина | Codext | 4 vCPU / 8GB RAM |
-| Фінляндія (Гельсінкі) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| США (Ешберн) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
| Україна (Київ) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
## Dev Container
diff --git a/docs/README-VN.md b/docs/README-VN.md
index 38a5df533..ea2c62ead 100644
--- a/docs/README-VN.md
+++ b/docs/README-VN.md
@@ -33,11 +33,7 @@ Dưới đây là những máy chủ mà bạn có thể sử dụng mà không
| Địa điểm | Nhà cung cấp | Cấu hình |
| --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
| Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
## Dependencies
diff --git a/docs/README-ZH.md b/docs/README-ZH.md
index b5ee85e72..7967f7d30 100644
--- a/docs/README-ZH.md
+++ b/docs/README-ZH.md
@@ -1,8 +1,8 @@
- 服务器 •
+ 服务器 •
编译 •
- Docker •
+ Docker •
结构 •
截图
[English ] | [Українська ] | [česky ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Deutsch ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ] | [Ελληνικά ]
@@ -16,9 +16,19 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
或者[自己设置](https://rustdesk.com/server),
亦或者[开发您的版本](https://github.com/rustdesk/rustdesk-server-demo)。
-欢迎大家贡献代码, 请看 [`docs/CONTRIBUTING.md`](CONTRIBUTING.md).
+
-[**可执行程序下载**](https://github.com/rustdesk/rustdesk/releases)
+RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](docs/CONTRIBUTING.md).
+
+[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
+
+[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
+
+[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
+
+[ ](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
## 免费的公共服务器
@@ -26,11 +36,16 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
-| Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
+| Germany | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
+| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
+
+## Dev Container
+
+[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
+
+如果你已经安装了 VS Code 和 Docker, 你可以点击上面的徽章开始使用. 点击后, VS Code 将自动安装 Dev Containers 扩展(如果需要),将源代码克隆到容器卷中, 并启动一个 Dev 容器供使用.
+
+Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info.
## 依赖
@@ -40,16 +55,14 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
-移动版本使用Flutter,未来会将桌面版本从Sciter迁移到Flutter。
-
## 基本构建步骤
-- 请准备好 Rust 开发环境和 C++编译环境
+- 请准备好 Rust 开发环境和 C++ 编译环境
-- 安装[vcpkg](https://github.com/microsoft/vcpkg), 正确设置`VCPKG_ROOT`环境变量
+- 安装 [vcpkg](https://github.com/microsoft/vcpkg), 正确设置 `VCPKG_ROOT` 环境变量
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- - Linux/Osx: vcpkg install libvpx libyuv opus aom
+ - Linux/macOS: vcpkg install libvpx libyuv opus aom
- 运行 `cargo run`
@@ -60,7 +73,15 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
### Ubuntu 18 (Debian 10)
```sh
-sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
+sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
+ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
+ libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
+```
+
+### openSUSE Tumbleweed
+
+```sh
+sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
```
### Fedora 28 (CentOS 8)
@@ -110,24 +131,52 @@ cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
mv libsciter-gtk.so target/debug
-cargo run
+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
+查看如何将 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容器
+克隆版本库并构建 Docker 容器:
```sh
git clone https://github.com/rustdesk/rustdesk # 克隆Github存储库
cd rustdesk # 进入文件夹
docker build -t "rustdesk-builder" . # 构建容器
```
+
请注意:
* 针对国内网络访问问题,可以做以下几点优化:
1. Dockerfile 中修改系统的源到国内镜像
@@ -166,8 +215,9 @@ docker build -t "rustdesk-builder" . # 构建容器
docker build -t "rustdesk-builder" . --build-arg http_proxy=http://host:port --build-arg https_proxy=http://host:port
```
-### 构建RustDesk程序
-容器构建完成后,运行下列指令以完成对RustDesk应用程序的构建:
+### 构建 RustDesk 程序
+
+然后, 每次需要构建应用程序时, 运行以下命令:
```sh
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
@@ -182,25 +232,25 @@ docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user
groupmod: Permission denied.
groupmod: cannot lock /etc/group; try again later.
```
- > **原因:** 容器的entrypoint脚本会检测UID和GID,在度判和给定的环境变量的不一致时,会强行修改user的UID和GID并重新运行。但在重启后读不到环境中的UID和GID,然后再次进入判错重启环节
+ > **原因:** 容器的 entrypoint 脚本会检测 UID 和 GID,在度判和给定的环境变量的不一致时,会强行修改 user 的 UID 和 GID 并重新运行。但在重启后读不到环境中的 UID 和 GID,然后再次进入判错重启环节
-### 运行RustDesk程序
+### 运行 RustDesk 程序
-生成的可执行程序在target目录下,可直接通过指令运行调试(Debug)版本的RustDesk:
+生成的可执行程序在 target 目录下,可直接通过指令运行调试 (Debug) 版本的 RustDesk:
```sh
target/debug/rustdesk
```
-或者您想运行发行(Release)版本:
+或者您想运行发行 (Release) 版本:
```sh
target/release/rustdesk
```
请注意:
-* 请保证您运行的目录是在RustDesk库的根目录内,否则软件会读不到文件。
-* `install`、`run`等Cargo的子指令在容器内不可用,宿主机才行。
+* 请保证您运行的目录是在 RustDesk 库的根目录内,否则软件会读不到文件。
+* `install`、`run`等 Cargo 的子指令在容器内不可用,宿主机才行。
## 文件结构
diff --git a/docs/SECURITY-IT.md b/docs/SECURITY-IT.md
new file mode 100644
index 000000000..91573dcf7
--- /dev/null
+++ b/docs/SECURITY-IT.md
@@ -0,0 +1,11 @@
+# Policy sicurezza
+
+## Segnalazione di una vulnerabilità
+
+Attribuiamo grande importanza alla sicurezza del progetto.
+Incoraggiamo tutti gli utenti a segnalare eventuali vulnerabilità di sicurezza che ci scoprono.
+Se trovi una vulnerabilità nel progetto RustDesk, segnalala responsabilmente inviando un'email a info@rustdesk.com.
+
+Al momento non abbiamo un programma di taglia sui bug.
+Siamo una piccola squadra che cerca di risolvere un grosso problema.
+Ti esortiamo a segnalare responsabilmente tutte le vulnerabilità in modo da poter continuare a sviluppare un'applicazione sicura per l'intera comunità.
diff --git a/docs/SECURITY-TR.md b/docs/SECURITY-TR.md
new file mode 100644
index 000000000..88037acb2
--- /dev/null
+++ b/docs/SECURITY-TR.md
@@ -0,0 +1,9 @@
+# Güvenlik Politikası
+
+## Bir Güvenlik Açığı Bildirme
+
+Projemiz için güvenliği çok önemsiyoruz. Kullanıcıların keşfettikleri herhangi bir güvenlik açığını bize bildirmelerini teşvik ediyoruz.
+Eğer RustDesk projesinde bir güvenlik açığı bulursanız, lütfen info@rustdesk.com adresine sorumlu bir şekilde bildirin.
+
+Şu an için bir hata ödül programımız bulunmamaktadır. Büyük bir sorunu çözmeye çalışan küçük bir ekibiz. Herhangi bir güvenlik açığını sorumlu bir şekilde bildirmenizi rica ederiz,
+böylece tüm topluluk için güvenli bir uygulama oluşturmaya devam edebiliriz.
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
index e84ed4d21..9b0125f21 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
index 5a83dc1f0..caffe504b 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
index 629631ac7..05b6afabb 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png
index 8e0a83a6a..bbef90fb9 100644
Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png differ
diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png
index 0618ae0b6..132736514 100644
Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png differ
diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png
index 560902b03..946956b2b 100644
Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png differ
diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json
index 3cfca4d30..4d2e297cc 100644
--- a/flatpak/rustdesk.json
+++ b/flatpak/rustdesk.json
@@ -4,7 +4,7 @@
"runtime-version": "21.08",
"sdk": "org.freedesktop.Sdk",
"command": "rustdesk",
- "icon": "share/rustdesk/files/rustdesk.png",
+ "icon": "share/icons/hicolor/scalable/apps/rustdesk.svg",
"modules": [
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
"xdotool.json",
@@ -12,20 +12,21 @@
"name": "rustdesk",
"buildsystem": "simple",
"build-commands": [
- "bsdtar -zxvf rustdesk-1.2.0.deb",
+ "bsdtar -zxvf rustdesk-1.2.3.deb",
"tar -xvf ./data.tar.xz",
"cp -r ./usr/* /app/",
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
"mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/com.rustdesk.RustDesk.desktop",
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/rustdesk-link.desktop",
+ "mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg",
"for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png logo.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
],
"cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
"sources": [
{
"type": "file",
- "path": "../rustdesk-1.2.0.deb"
+ "path": "../rustdesk-1.2.3.deb"
},
{
"type": "file",
diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle
index 326689e5e..f4dc69e41 100644
--- a/flutter/android/app/build.gradle
+++ b/flutter/android/app/build.gradle
@@ -46,7 +46,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.carriez.flutter_hbb"
minSdkVersion 21
- targetSdkVersion 31
+ targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
index 905a2734d..203558968 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
@@ -26,6 +26,13 @@ const val WHEEL_BUTTON_UP = 34
const val WHEEL_DOWN = 523331
const val WHEEL_UP = 963
+const val TOUCH_SCALE_START = 1
+const val TOUCH_SCALE = 2
+const val TOUCH_SCALE_END = 3
+const val TOUCH_PAN_START = 4
+const val TOUCH_PAN_UPDATE = 5
+const val TOUCH_PAN_END = 6
+
const val WHEEL_STEP = 120
const val WHEEL_DURATION = 50L
const val LONG_TAP_DELAY = 200L
@@ -167,6 +174,30 @@ class InputService : AccessibilityService() {
}
}
+ @RequiresApi(Build.VERSION_CODES.N)
+ fun onTouchInput(mask: Int, _x: Int, _y: Int) {
+ when (mask) {
+ TOUCH_PAN_UPDATE -> {
+ mouseX -= _x * SCREEN_INFO.scale
+ mouseY -= _y * SCREEN_INFO.scale
+ mouseX = max(0, mouseX);
+ mouseY = max(0, mouseY);
+ continueGesture(mouseX, mouseY)
+ }
+ TOUCH_PAN_START -> {
+ mouseX = max(0, _x) * SCREEN_INFO.scale
+ mouseY = max(0, _y) * SCREEN_INFO.scale
+ startGesture(mouseX, mouseY)
+ }
+ TOUCH_PAN_END -> {
+ endGesture(mouseX, mouseY)
+ mouseX = max(0, _x) * SCREEN_INFO.scale
+ mouseY = max(0, _y) * SCREEN_INFO.scale
+ }
+ else -> {}
+ }
+ }
+
@RequiresApi(Build.VERSION_CODES.N)
private fun consumeWheelActions() {
if (isWheelActionsPolling) {
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
index 78e4e451e..535a3f8c3 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
@@ -71,17 +71,26 @@ class MainService : Service() {
@Keep
@RequiresApi(Build.VERSION_CODES.N)
- fun rustMouseInput(mask: Int, x: Int, y: Int) {
+ fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
// turn on screen with LIFT_DOWN when screen off
- if (!powerManager.isInteractive && mask == LIFT_DOWN) {
+ if (!powerManager.isInteractive && (kind == "touch" || mask == LIFT_DOWN)) {
if (wakeLock.isHeld) {
- Log.d(logTag,"Turn on Screen, WakeLock release")
+ Log.d(logTag, "Turn on Screen, WakeLock release")
wakeLock.release()
}
Log.d(logTag,"Turn on Screen")
wakeLock.acquire(5000)
} else {
- InputService.ctx?.onMouseInput(mask,x,y)
+ when (kind) {
+ "touch" -> {
+ InputService.ctx?.onTouchInput(mask, x, y)
+ }
+ "mouse" -> {
+ InputService.ctx?.onMouseInput(mask, x, y)
+ }
+ else -> {
+ }
+ }
}
}
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index d05404d3a..116904a84 100644
Binary files a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
index 3742f241f..7c8a2be8d 100644
Binary files a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
index 964c5faa0..c8aef7fd5 100644
Binary files a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png
index 79a814f59..42e74fe0b 100644
Binary files a/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png
index 814ba4549..60e95748c 100644
Binary files a/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index f16b3d61d..7dca207d8 100644
Binary files a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
index de17ccbda..0faadfca0 100644
Binary files a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
index 2136a2f3c..246d6ee7a 100644
Binary files a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png
index c179bf053..d643a4fac 100644
Binary files a/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index d9bd8fdfe..33a40ed83 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
index f8ced45f1..e083cecf1 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index 415eca622..ab9c356dc 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png
index d82d1a81b..e02182dd1 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index eba179347..4585230d3 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
index 0f46fafaf..b9975190c 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index 87889c953..79eab3d84 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png
index 2cbe6eaf1..f7b57b329 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index a8d80d2a2..463d20eeb 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
index 88eafe8dd..d781e2595 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index 00709a815..3cae0ce40 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png
index 209c5f977..9c2153e9e 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png differ
diff --git a/flutter/assets/GitHub.svg b/flutter/assets/GitHub.svg
deleted file mode 100644
index ef0bb12a7..000000000
--- a/flutter/assets/GitHub.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/flutter/assets/Google.svg b/flutter/assets/Google.svg
deleted file mode 100644
index df394a84f..000000000
--- a/flutter/assets/Google.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/flutter/assets/auth-apple.svg b/flutter/assets/auth-apple.svg
new file mode 100644
index 000000000..6933fbc3b
--- /dev/null
+++ b/flutter/assets/auth-apple.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/auth-auth0.svg b/flutter/assets/auth-auth0.svg
new file mode 100644
index 000000000..dbe3ed236
--- /dev/null
+++ b/flutter/assets/auth-auth0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/auth-azure.svg b/flutter/assets/auth-azure.svg
new file mode 100644
index 000000000..b7435604d
--- /dev/null
+++ b/flutter/assets/auth-azure.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/auth-default.svg b/flutter/assets/auth-default.svg
new file mode 100644
index 000000000..bf5fa9073
--- /dev/null
+++ b/flutter/assets/auth-default.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/auth-facebook.svg b/flutter/assets/auth-facebook.svg
new file mode 100644
index 000000000..f58725000
--- /dev/null
+++ b/flutter/assets/auth-facebook.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/auth-github.svg b/flutter/assets/auth-github.svg
new file mode 100644
index 000000000..778b7b341
--- /dev/null
+++ b/flutter/assets/auth-github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/auth-gitlab.svg b/flutter/assets/auth-gitlab.svg
new file mode 100644
index 000000000..9402e1329
--- /dev/null
+++ b/flutter/assets/auth-gitlab.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/auth-google.svg b/flutter/assets/auth-google.svg
new file mode 100644
index 000000000..18970f31a
--- /dev/null
+++ b/flutter/assets/auth-google.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/Okta.svg b/flutter/assets/auth-okta.svg
similarity index 100%
rename from flutter/assets/Okta.svg
rename to flutter/assets/auth-okta.svg
diff --git a/flutter/assets/checkbox.ttf b/flutter/assets/checkbox.ttf
new file mode 100644
index 000000000..70ddde698
Binary files /dev/null and b/flutter/assets/checkbox.ttf differ
diff --git a/flutter/assets/file_transfer.svg b/flutter/assets/file_transfer.svg
new file mode 100644
index 000000000..e1d8ccbec
--- /dev/null
+++ b/flutter/assets/file_transfer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/build_ios.sh b/flutter/build_ios.sh
index 6d0d627ac..a6468a0a8 100755
--- a/flutter/build_ios.sh
+++ b/flutter/build_ios.sh
@@ -1,2 +1,5 @@
#!/usr/bin/env bash
-flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
+# https://docs.flutter.dev/deployment/ios
+# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
+# no obfuscate, because no easy to check errors
+flutter build ipa --release
diff --git a/flutter/ios/Podfile.lock b/flutter/ios/Podfile.lock
index 76d0bac73..1ad5f6360 100644
--- a/flutter/ios/Podfile.lock
+++ b/flutter/ios/Podfile.lock
@@ -75,7 +75,7 @@ DEPENDENCIES:
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
+ - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- uni_links (from `.symlinks/plugins/uni_links/ios`)
@@ -106,7 +106,7 @@ EXTERNAL SOURCES:
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
- :path: ".symlinks/plugins/path_provider_foundation/ios"
+ :path: ".symlinks/plugins/path_provider_foundation/darwin"
qr_code_scanner:
:path: ".symlinks/plugins/qr_code_scanner/ios"
sqflite:
@@ -141,6 +141,6 @@ SPEC CHECKSUMS:
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
-PODFILE CHECKSUM: c649b4e69a3086d323110011d04604e416ad0dcd
+PODFILE CHECKSUM: 2aff76ba0ac13439479560d1d03e9b4479f5c9e1
-COCOAPODS: 1.12.0
+COCOAPODS: 1.12.1
diff --git a/flutter/ios/Runner.xcodeproj/project.pbxproj b/flutter/ios/Runner.xcodeproj/project.pbxproj
index a3bc7d43d..0813abb11 100644
--- a/flutter/ios/Runner.xcodeproj/project.pbxproj
+++ b/flutter/ios/Runner.xcodeproj/project.pbxproj
@@ -208,6 +208,7 @@
files = (
);
inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@@ -437,6 +438,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
PRODUCT_NAME = "$(TARGET_NAME)";
+ STRIP_STYLE = "non-global";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -634,6 +636,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
PRODUCT_NAME = "$(TARGET_NAME)";
+ STRIP_STYLE = "non-global";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -723,6 +726,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
PRODUCT_NAME = "$(TARGET_NAME)";
+ STRIP_STYLE = "non-global";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
diff --git a/flutter/ios/Runner/AppDelegate.swift b/flutter/ios/Runner/AppDelegate.swift
index 06a9a2695..730c9adcb 100644
--- a/flutter/ios/Runner/AppDelegate.swift
+++ b/flutter/ios/Runner/AppDelegate.swift
@@ -13,9 +13,7 @@ import Flutter
}
public func dummyMethodToEnforceBundling() {
- get_rgba();
- // free_rgba(nil);
- // get_by_name("", "");
- // set_by_name("", "");
+ dummy_method_to_enforce_bundling();
+ session_get_rgba(nil);
}
}
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
index eabd8512d..53611299a 100644
--- a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,122 +1,122 @@
{
- "images": [
+ "images" : [
{
- "filename": "Icon-App-20x20@2x.png",
- "idiom": "iphone",
- "scale": "2x",
- "size": "20x20"
+ "filename" : "Icon-App-20x20@2x.png",
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "20x20"
},
{
- "filename": "Icon-App-20x20@3x.png",
- "idiom": "iphone",
- "scale": "3x",
- "size": "20x20"
+ "filename" : "Icon-App-20x20@3x.png",
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "20x20"
},
{
- "filename": "Icon-App-29x29@1x.png",
- "idiom": "iphone",
- "scale": "1x",
- "size": "29x29"
+ "filename" : "Icon-App-29x29@1x.png",
+ "idiom" : "iphone",
+ "scale" : "1x",
+ "size" : "29x29"
},
{
- "filename": "Icon-App-29x29@2x.png",
- "idiom": "iphone",
- "scale": "2x",
- "size": "29x29"
+ "filename" : "Icon-App-29x29@2x.png",
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "29x29"
},
{
- "filename": "Icon-App-29x29@3x.png",
- "idiom": "iphone",
- "scale": "3x",
- "size": "29x29"
+ "filename" : "Icon-App-29x29@3x.png",
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "29x29"
},
{
- "filename": "Icon-App-40x40@2x.png",
- "idiom": "iphone",
- "scale": "2x",
- "size": "40x40"
+ "filename" : "Icon-App-40x40@2x.png",
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "40x40"
},
{
- "filename": "Icon-App-40x40@3x.png",
- "idiom": "iphone",
- "scale": "3x",
- "size": "40x40"
+ "filename" : "Icon-App-40x40@3x.png",
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "40x40"
},
{
- "filename": "Icon-App-60x60@2x.png",
- "idiom": "iphone",
- "scale": "2x",
- "size": "60x60"
+ "filename" : "Icon-App-60x60@2x.png",
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "60x60"
},
{
- "filename": "Icon-App-60x60@3x.png",
- "idiom": "iphone",
- "scale": "3x",
- "size": "60x60"
+ "filename" : "Icon-App-60x60@3x.png",
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "60x60"
},
{
- "filename": "Icon-App-20x20@1x.png",
- "idiom": "ipad",
- "scale": "1x",
- "size": "20x20"
+ "filename" : "Icon-App-20x20@1x.png",
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "20x20"
},
{
- "filename": "Icon-App-20x20@2x.png",
- "idiom": "ipad",
- "scale": "2x",
- "size": "20x20"
+ "filename" : "Icon-App-20x20@2x.png",
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "20x20"
},
{
- "filename": "Icon-App-29x29@1x.png",
- "idiom": "ipad",
- "scale": "1x",
- "size": "29x29"
+ "filename" : "Icon-App-29x29@1x.png",
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "29x29"
},
{
- "filename": "Icon-App-29x29@2x.png",
- "idiom": "ipad",
- "scale": "2x",
- "size": "29x29"
+ "filename" : "Icon-App-29x29@2x.png",
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "29x29"
},
{
- "filename": "Icon-App-40x40@1x.png",
- "idiom": "ipad",
- "scale": "1x",
- "size": "40x40"
+ "filename" : "Icon-App-40x40@1x.png",
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "40x40"
},
{
- "filename": "Icon-App-40x40@2x.png",
- "idiom": "ipad",
- "scale": "2x",
- "size": "40x40"
+ "filename" : "Icon-App-40x40@2x.png",
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "40x40"
},
{
- "filename": "Icon-App-76x76@1x.png",
- "idiom": "ipad",
- "scale": "1x",
- "size": "76x76"
+ "filename" : "Icon-App-76x76@1x.png",
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "76x76"
},
{
- "filename": "Icon-App-76x76@2x.png",
- "idiom": "ipad",
- "scale": "2x",
- "size": "76x76"
+ "filename" : "Icon-App-76x76@2x.png",
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "76x76"
},
{
- "filename": "Icon-App-83.5x83.5@2x.png",
- "idiom": "ipad",
- "scale": "2x",
- "size": "83.5x83.5"
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "83.5x83.5"
},
{
- "filename": "Icon-App-1024x1024@1x.png",
- "idiom": "ios-marketing",
- "scale": "1x",
- "size": "1024x1024"
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "idiom" : "ios-marketing",
+ "scale" : "1x",
+ "size" : "1024x1024"
}
],
- "info": {
- "author": "icons_launcher",
- "version": 1
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
}
-}
\ No newline at end of file
+}
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index 16cef3177..fa4ecad0a 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index 298f4d9af..080f311fc 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index fd3b01b6d..8daf5718b 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index 18ebaab69..fa5b04f7f 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index a8ee14a31..a4bc6dfd7 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index a83f88b05..97831dc82 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index 331e72531..cfaff2b9b 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index fd3b01b6d..8daf5718b 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index aee7e4321..85365e185 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index 2d0da17b1..bafaf7e07 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index 2d0da17b1..bafaf7e07 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index 7ee56922e..e5755e5fb 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index 76abd423b..b60a754df 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index e08138333..0ddd120e6 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 46de51af6..22628c536 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
index 0bedcf2fd..00cabce83 100644
--- a/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
+++ b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -1,23 +1,23 @@
{
"images" : [
{
- "idiom" : "universal",
"filename" : "LaunchImage.png",
+ "idiom" : "universal",
"scale" : "1x"
},
{
- "idiom" : "universal",
"filename" : "LaunchImage@2x.png",
+ "idiom" : "universal",
"scale" : "2x"
},
{
- "idiom" : "universal",
"filename" : "LaunchImage@3x.png",
+ "idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
- "version" : 1,
- "author" : "xcode"
+ "author" : "xcode",
+ "version" : 1
}
}
diff --git a/flutter/ios/Runner/Base.lproj/Main.storyboard b/flutter/ios/Runner/Base.lproj/Main.storyboard
index f3c28516f..d68a3a7a5 100644
--- a/flutter/ios/Runner/Base.lproj/Main.storyboard
+++ b/flutter/ios/Runner/Base.lproj/Main.storyboard
@@ -1,8 +1,10 @@
-
-
+
+
+
-
+
+
@@ -14,13 +16,14 @@
-
+
-
+
+
diff --git a/flutter/ios/Runner/Runner-Bridging-Header.h b/flutter/ios/Runner/Runner-Bridging-Header.h
index a8c447418..e930a3997 100644
--- a/flutter/ios/Runner/Runner-Bridging-Header.h
+++ b/flutter/ios/Runner/Runner-Bridging-Header.h
@@ -1,3 +1,3 @@
#import "GeneratedPluginRegistrant.h"
-#import "ffi.h"
+#import "bridge_generated.h"
diff --git a/flutter/ios/Runner/ffi.h b/flutter/ios/Runner/ffi.h
deleted file mode 100644
index 701ec4b09..000000000
--- a/flutter/ios/Runner/ffi.h
+++ /dev/null
@@ -1,4 +0,0 @@
-void* get_rgba();
-void free_rgba(void*);
-void set_by_name(const char*, const char*);
-const char* get_by_name(const char*, const char*);
diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index 41fa826b8..33321c81a 100644
--- a/flutter/lib/common.dart
+++ b/flutter/lib/common.dart
@@ -11,15 +11,16 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
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/main.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';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
-import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
import 'package:uni_links/uni_links.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:uuid/uuid.dart';
@@ -47,11 +48,6 @@ var isMobile = isAndroid || isIOS;
var version = "";
int androidVersion = 0;
-/// Incriment count for textureId.
-int _textureId = 0;
-int get newTextureId => _textureId++;
-final textureRenderer = TextureRgbaRenderer();
-
/// only available for Windows target
int windowsBuildNumber = 0;
DesktopType? desktopType;
@@ -95,6 +91,7 @@ class IconFont {
static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
static const IconData addressBook =
IconData(0xe602, fontFamily: "AddressBook");
+ static const IconData checkbox = IconData(0xe7d6, fontFamily: "CheckBox");
}
class ColorThemeExtension extends ThemeExtension {
@@ -176,6 +173,10 @@ class MyTheme {
static const Color dark = Colors.black87;
static const Color button = Color(0xFF2C8CFF);
static const Color hoverBorder = Color(0xFF999999);
+ static const Color bordDark = Colors.white24;
+ static const Color bordLight = Colors.black26;
+ static const Color dividerDark = Colors.white38;
+ static const Color dividerLight = Colors.black38;
// ListTile
static const ListTileThemeData listTileTheme = ListTileThemeData(
@@ -219,6 +220,13 @@ class MyTheme {
),
);
+ //tooltip
+ static TooltipThemeData tooltipTheme() {
+ return TooltipThemeData(
+ waitDuration: Duration(seconds: 1, milliseconds: 500),
+ );
+ }
+
// Dialogs
static const double dialogPadding = 24;
@@ -288,6 +296,7 @@ class MyTheme {
tabBarTheme: const TabBarTheme(
labelColor: Colors.black87,
),
+ tooltipTheme: tooltipTheme(),
splashColor: isDesktop ? Colors.transparent : null,
highlightColor: isDesktop ? Colors.transparent : null,
splashFactory: isDesktop ? NoSplash.splashFactory : null,
@@ -377,6 +386,7 @@ class MyTheme {
scrollbarTheme: ScrollbarThemeData(
thumbColor: MaterialStateProperty.all(Colors.grey[500]),
),
+ tooltipTheme: tooltipTheme(),
splashColor: isDesktop ? Colors.transparent : null,
highlightColor: isDesktop ? Colors.transparent : null,
splashFactory: isDesktop ? NoSplash.splashFactory : null,
@@ -545,17 +555,19 @@ closeConnection({String? id}) {
}
}
-void window_on_top(int? id) {
+void windowOnTop(int? id) async {
if (!isDesktop) {
return;
}
+ print("Bring window '$id' on top");
if (id == null) {
- print("Bring window on top");
// main window
- windowManager.restore();
- windowManager.show();
- windowManager.focus();
- rustDeskWinManager.registerActiveWindow(kWindowMainId);
+ if (stateGlobal.isMinimized) {
+ await windowManager.restore();
+ }
+ await windowManager.show();
+ await windowManager.focus();
+ await rustDeskWinManager.registerActiveWindow(kWindowMainId);
} else {
WindowController.fromWindowId(id)
..focus()
@@ -602,6 +614,7 @@ class OverlayDialogManager {
int _tagCount = 0;
OverlayEntry? _mobileActionsOverlayEntry;
+ RxBool mobileActionsOverlayVisible = false.obs;
void setOverlayState(OverlayKeyState overlayKeyState) {
_overlayKeyState = overlayKeyState;
@@ -685,9 +698,12 @@ class OverlayDialogManager {
String showLoading(String text,
{bool clickMaskDismiss = false,
bool showCancel = true,
- VoidCallback? onCancel}) {
- final tag = _tagCount.toString();
- _tagCount++;
+ VoidCallback? onCancel,
+ String? tag}) {
+ if (tag == null) {
+ tag = _tagCount.toString();
+ _tagCount++;
+ }
show((setState, close, context) {
cancel() {
dismissAll();
@@ -765,12 +781,14 @@ class OverlayDialogManager {
});
overlayState.insert(overlay);
_mobileActionsOverlayEntry = overlay;
+ mobileActionsOverlayVisible.value = true;
}
void hideMobileActionsOverlay() {
if (_mobileActionsOverlayEntry != null) {
_mobileActionsOverlayEntry!.remove();
_mobileActionsOverlayEntry = null;
+ mobileActionsOverlayVisible.value = false;
return;
}
}
@@ -1062,6 +1080,45 @@ Color str2color(String str, [alpha = 0xFF]) {
return Color((hash & 0xFF7FFF) | (alpha << 24));
}
+Color str2color2(String str, {List existing = const []}) {
+ Map colorMap = {
+ "red": Colors.red,
+ "green": Colors.green,
+ "blue": Colors.blue,
+ "orange": Colors.orange,
+ "purple": Colors.purple,
+ "grey": Colors.grey,
+ "cyan": Colors.cyan,
+ "lime": Colors.lime,
+ "teal": Colors.teal,
+ "pink": Colors.pink[200]!,
+ "indigo": Colors.indigo,
+ "brown": Colors.brown,
+ };
+ final color = colorMap[str.toLowerCase()];
+ if (color != null) {
+ return color.withAlpha(0xFF);
+ }
+ if (str.toLowerCase() == 'yellow') {
+ return Colors.yellow.withAlpha(0xFF);
+ }
+ var hash = 0;
+ for (var i = 0; i < str.length; i++) {
+ hash += str.codeUnitAt(i);
+ }
+ List colorList = colorMap.values.toList();
+ hash = hash % colorList.length;
+ var result = colorList[hash].withAlpha(0xFF);
+ if (existing.contains(result.value)) {
+ Color? notUsed =
+ colorList.firstWhereOrNull((e) => !existing.contains(e.value));
+ if (notUsed != null) {
+ result = notUsed;
+ }
+ }
+ return result;
+}
+
const K = 1024;
const M = K * K;
const G = M * K;
@@ -1218,7 +1275,7 @@ FFI get gFFI => _globalFFI;
Future initGlobalFFI() async {
debugPrint("_globalFFI init");
- _globalFFI = FFI();
+ _globalFFI = FFI(null);
debugPrint("_globalFFI init end");
// after `put`, can also be globally found by Get.find();
Get.put(_globalFFI, permanent: true);
@@ -1239,7 +1296,7 @@ bool option2bool(String option, String value) {
option == "stop-service" ||
option == "direct-server" ||
option == "stop-rendezvous-service" ||
- option == "force-always-relay") {
+ option == kOptionForceAlwaysRelay) {
res = value == "Y";
} else {
assert(false);
@@ -1256,7 +1313,7 @@ String bool2option(String option, bool b) {
option == "stop-service" ||
option == "direct-server" ||
option == "stop-rendezvous-service" ||
- option == "force-always-relay") {
+ option == kOptionForceAlwaysRelay) {
res = b ? 'Y' : '';
} else {
assert(false);
@@ -1265,6 +1322,36 @@ String bool2option(String option, bool b) {
return res;
}
+mainSetBoolOption(String key, bool value) async {
+ String v = bool2option(key, value);
+ await bind.mainSetOption(key: key, value: v);
+}
+
+Future mainGetBoolOption(String key) async {
+ return option2bool(key, await bind.mainGetOption(key: key));
+}
+
+bool mainGetBoolOptionSync(String key) {
+ return option2bool(key, bind.mainGetOptionSync(key: key));
+}
+
+mainSetLocalBoolOption(String key, bool value) async {
+ String v = bool2option(key, value);
+ await bind.mainSetLocalOption(key: key, value: v);
+}
+
+bool mainGetLocalBoolOptionSync(String key) {
+ return option2bool(key, bind.mainGetLocalOption(key: key));
+}
+
+bool mainGetPeerBoolOptionSync(String id, String key) {
+ return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key));
+}
+
+mainSetPeerBoolOptionSync(String id, String key, bool v) {
+ bind.mainSetPeerOptionSync(id: id, key: key, value: bool2option(key, v));
+}
+
Future matchPeer(String searchText, Peer peer) async {
if (searchText.isEmpty) {
return true;
@@ -1276,7 +1363,7 @@ Future matchPeer(String searchText, Peer peer) async {
peer.username.toLowerCase().contains(searchText)) {
return true;
}
- final alias = await bind.mainGetPeerOption(id: peer.id, key: 'alias');
+ final alias = peer.alias;
if (alias.isEmpty) {
return false;
}
@@ -1305,9 +1392,10 @@ class LastWindowPosition {
double? offsetWidth;
double? offsetHeight;
bool? isMaximized;
+ bool? isFullscreen;
LastWindowPosition(this.width, this.height, this.offsetWidth,
- this.offsetHeight, this.isMaximized);
+ this.offsetHeight, this.isMaximized, this.isFullscreen);
Map toJson() {
return {
@@ -1316,6 +1404,7 @@ class LastWindowPosition {
"offsetWidth": offsetWidth,
"offsetHeight": offsetHeight,
"isMaximized": isMaximized,
+ "isFullscreen": isFullscreen,
};
}
@@ -1331,9 +1420,11 @@ class LastWindowPosition {
try {
final m = jsonDecode(content);
return LastWindowPosition(m["width"], m["height"], m["offsetWidth"],
- m["offsetHeight"], m["isMaximized"]);
+ m["offsetHeight"], m["isMaximized"], m["isFullscreen"]);
} catch (e) {
- debugPrintStack(label: e.toString());
+ debugPrintStack(
+ label:
+ 'Failed to load LastWindowPosition "$content" ${e.toString()}');
return null;
}
}
@@ -1346,18 +1437,32 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async {
debugPrint(
"Error: windowId cannot be null when saving positions for sub window");
}
+
+ late Offset position;
+ late Size sz;
+ late bool isMaximized;
+ bool isFullscreen = stateGlobal.fullscreen ||
+ (Platform.isMacOS && stateGlobal.closeOnFullscreen);
+ setFrameIfMaximized() {
+ if (isMaximized) {
+ final pos = bind.getLocalFlutterOption(k: kWindowPrefix + type.name);
+ var lpos = LastWindowPosition.loadFromString(pos);
+ position = Offset(
+ lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
+ sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
+ }
+ }
+
switch (type) {
case WindowType.Main:
- final position = await windowManager.getPosition();
- final sz = await windowManager.getSize();
- final isMaximized = await windowManager.isMaximized();
- final pos = LastWindowPosition(
- sz.width, sz.height, position.dx, position.dy, isMaximized);
- await bind.setLocalFlutterConfig(
- k: kWindowPrefix + type.name, v: pos.toString());
+ isMaximized = await windowManager.isMaximized();
+ position = await windowManager.getPosition();
+ sz = await windowManager.getSize();
+ setFrameIfMaximized();
break;
default:
final wc = WindowController.fromWindowId(windowId!);
+ isMaximized = await wc.isMaximized();
final Rect frame;
try {
frame = await wc.getFrame();
@@ -1365,38 +1470,74 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async {
debugPrint("Failed to get frame of window $windowId, it may be hidden");
return;
}
- final position = frame.topLeft;
- final sz = frame.size;
- final isMaximized = await wc.isMaximized();
- final pos = LastWindowPosition(
- sz.width, sz.height, position.dx, position.dy, isMaximized);
- debugPrint(
- "saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}");
- await bind.setLocalFlutterConfig(
- k: kWindowPrefix + type.name, v: pos.toString());
+ position = frame.topLeft;
+ sz = frame.size;
+ setFrameIfMaximized();
break;
}
+ if (Platform.isWindows) {
+ const kMinOffset = -10000;
+ const kMaxOffset = 10000;
+ if (position.dx < kMinOffset ||
+ position.dy < kMinOffset ||
+ position.dx > kMaxOffset ||
+ position.dy > kMaxOffset) {
+ debugPrint("Invalid position: $position, ignore saving position");
+ return;
+ }
+ }
+
+ final pos = LastWindowPosition(
+ sz.width, sz.height, position.dx, position.dy, isMaximized, isFullscreen);
+ debugPrint(
+ "Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}");
+
+ await bind.setLocalFlutterOption(
+ k: kWindowPrefix + type.name, v: pos.toString());
+
+ if (type == WindowType.RemoteDesktop && windowId != null) {
+ await _saveSessionWindowPosition(
+ type, windowId, isMaximized, isFullscreen, pos);
+ }
+}
+
+Future _saveSessionWindowPosition(WindowType windowType, int windowId,
+ bool isMaximized, bool isFullscreen, LastWindowPosition pos) async {
+ final remoteList = await DesktopMultiWindow.invokeMethod(
+ windowId, kWindowEventGetRemoteList, null);
+ getPeerPos(String peerId) {
+ if (isMaximized) {
+ final peerPos = bind.mainGetPeerFlutterOptionSync(
+ id: peerId, k: kWindowPrefix + windowType.name);
+ var lpos = LastWindowPosition.loadFromString(peerPos);
+ return LastWindowPosition(
+ lpos?.width ?? pos.offsetWidth,
+ lpos?.height ?? pos.offsetHeight,
+ lpos?.offsetWidth ?? pos.offsetWidth,
+ lpos?.offsetHeight ?? pos.offsetHeight,
+ isMaximized,
+ isFullscreen)
+ .toString();
+ } else {
+ return pos.toString();
+ }
+ }
+
+ if (remoteList != null) {
+ for (final peerId in remoteList.split(',')) {
+ bind.mainSetPeerFlutterOptionSync(
+ id: peerId,
+ k: kWindowPrefix + windowType.name,
+ v: getPeerPos(peerId));
+ }
+ }
}
Future _adjustRestoreMainWindowSize(double? width, double? height) async {
- const double minWidth = 600;
- const double minHeight = 100;
- double maxWidth = (((isDesktop || isWebDesktop)
- ? kDesktopMaxDisplayWidth
- : kMobileMaxDisplayWidth))
- .toDouble();
- double maxHeight = ((isDesktop || isWebDesktop)
- ? kDesktopMaxDisplayHeight
- : kMobileMaxDisplayHeight)
- .toDouble();
-
- if (isDesktop || isWebDesktop) {
- final screen = (await window_size.getWindowInfo()).screen;
- if (screen != null) {
- maxWidth = screen.visibleFrame.width;
- maxHeight = screen.visibleFrame.height;
- }
- }
+ const double minWidth = 1;
+ const double minHeight = 1;
+ const double maxWidth = 6480;
+ const double maxHeight = 6480;
final defaultWidth =
((isDesktop || isWebDesktop) ? 1280 : kMobileDefaultDisplayWidth)
@@ -1408,64 +1549,79 @@ Future _adjustRestoreMainWindowSize(double? width, double? height) async {
double restoreHeight = height ?? defaultHeight;
if (restoreWidth < minWidth) {
- restoreWidth = minWidth;
+ restoreWidth = defaultWidth;
}
if (restoreHeight < minHeight) {
- restoreHeight = minHeight;
+ restoreHeight = defaultHeight;
}
if (restoreWidth > maxWidth) {
- restoreWidth = maxWidth;
+ restoreWidth = defaultWidth;
}
if (restoreHeight > maxHeight) {
- restoreHeight = maxHeight;
+ restoreHeight = defaultHeight;
}
return Size(restoreWidth, restoreHeight);
}
/// return null means center
Future _adjustRestoreMainWindowOffset(
- double? left, double? top) async {
- if (left == null || top == null) {
- await windowManager.center();
- } else {
- double windowLeft = max(0.0, left);
- double windowTop = max(0.0, top);
+ double? left,
+ double? top,
+ double? width,
+ double? height,
+) async {
+ if (left == null || top == null || width == null || height == null) {
+ return null;
+ }
- double frameLeft = double.infinity;
- double frameTop = double.infinity;
- double frameRight = ((isDesktop || isWebDesktop)
- ? kDesktopMaxDisplayWidth
- : kMobileMaxDisplayWidth)
- .toDouble();
- double frameBottom = ((isDesktop || isWebDesktop)
- ? kDesktopMaxDisplayHeight
- : kMobileMaxDisplayHeight)
- .toDouble();
+ double? frameLeft;
+ double? frameTop;
+ double? frameRight;
+ double? frameBottom;
- if (isDesktop || isWebDesktop) {
- for (final screen in await window_size.getScreenList()) {
- frameLeft = min(screen.visibleFrame.left, frameLeft);
- frameTop = min(screen.visibleFrame.top, frameTop);
- frameRight = max(screen.visibleFrame.right, frameRight);
- frameBottom = max(screen.visibleFrame.bottom, frameBottom);
- }
- }
-
- if (windowLeft < frameLeft ||
- windowLeft > frameRight ||
- windowTop < frameTop ||
- windowTop > frameBottom) {
- return null;
- } else {
- return Offset(windowLeft, windowTop);
+ if (isDesktop || isWebDesktop) {
+ for (final screen in await window_size.getScreenList()) {
+ frameLeft = frameLeft == null
+ ? screen.visibleFrame.left
+ : min(screen.visibleFrame.left, frameLeft);
+ frameTop = frameTop == null
+ ? screen.visibleFrame.top
+ : min(screen.visibleFrame.top, frameTop);
+ frameRight = frameRight == null
+ ? screen.visibleFrame.right
+ : max(screen.visibleFrame.right, frameRight);
+ frameBottom = frameBottom == null
+ ? screen.visibleFrame.bottom
+ : max(screen.visibleFrame.bottom, frameBottom);
}
}
- return null;
+ if (frameLeft == null) {
+ frameLeft = 0.0;
+ frameTop = 0.0;
+ frameRight = ((isDesktop || isWebDesktop)
+ ? kDesktopMaxDisplaySize
+ : kMobileMaxDisplaySize)
+ .toDouble();
+ frameBottom = ((isDesktop || isWebDesktop)
+ ? kDesktopMaxDisplaySize
+ : kMobileMaxDisplaySize)
+ .toDouble();
+ }
+ final minWidth = 10.0;
+ if ((left + minWidth) > frameRight! ||
+ (top + minWidth) > frameBottom! ||
+ (left + width - minWidth) < frameLeft ||
+ top < frameTop!) {
+ return null;
+ } else {
+ return Offset(left, top);
+ }
}
/// Restore window position and size on start
/// Note that windowId must be provided if it's subwindow
-Future restoreWindowPosition(WindowType type, {int? windowId}) async {
+Future restoreWindowPosition(WindowType type,
+ {int? windowId, String? peerId}) async {
if (bind
.mainGetEnv(key: "DISABLE_RUSTDESK_RESTORE_WINDOW_POSITION")
.isNotEmpty) {
@@ -1474,42 +1630,74 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async {
if (type != WindowType.Main && windowId == null) {
debugPrint(
"Error: windowId cannot be null when saving positions for sub window");
+ return false;
}
- final pos = bind.getLocalFlutterConfig(k: kWindowPrefix + type.name);
+
+ bool isRemotePeerPos = false;
+ String? pos;
+ // No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
+ // Though "open in tabs" is true and the new window restore peer position, it's ok.
+ if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
+ // If the restore position is called by main window, and the peer id is not null
+ // then we may need to get the position by reading the peer config.
+ // Because the session may not be read at this time.
+ if (desktopType == DesktopType.main) {
+ pos = bind.mainGetPeerFlutterOptionSync(
+ id: peerId, k: kWindowPrefix + type.name);
+ } else {
+ pos = await bind.sessionGetFlutterOptionByPeerId(
+ id: peerId, k: kWindowPrefix + type.name);
+ }
+ isRemotePeerPos = pos != null;
+ }
+ pos ??= bind.getLocalFlutterOption(k: kWindowPrefix + type.name);
+
var lpos = LastWindowPosition.loadFromString(pos);
if (lpos == null) {
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 (lpos.offsetHeight != null) {
+ lpos.offsetHeight = lpos.offsetHeight! + windowId * 20;
+ }
+ }
+
+ final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
+ final offset = await _adjustRestoreMainWindowOffset(
+ lpos.offsetWidth,
+ lpos.offsetHeight,
+ size.width,
+ size.height,
+ );
+ debugPrint(
+ "restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}");
switch (type) {
case WindowType.Main:
- if (lpos.isMaximized == true) {
- await windowManager.maximize();
- } else {
- final size =
- await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
- final offset = await _adjustRestoreMainWindowOffset(
- lpos.offsetWidth, lpos.offsetHeight);
- await windowManager.setSize(size);
+ restorePos() async {
if (offset == null) {
await windowManager.center();
} else {
await windowManager.setPosition(offset);
}
}
+ if (lpos.isMaximized == true) {
+ await restorePos();
+ await windowManager.maximize();
+ } else {
+ await windowManager.setSize(size);
+ await restorePos();
+ }
return true;
default:
final wc = WindowController.fromWindowId(windowId!);
- if (lpos.isMaximized == true) {
- await wc.maximize();
- } else {
- final size =
- await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
- final offset = await _adjustRestoreMainWindowOffset(
- lpos.offsetWidth, lpos.offsetHeight);
- debugPrint(
- "restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}");
+ restoreFrame() async {
if (offset == null) {
await wc.center();
} else {
@@ -1518,6 +1706,21 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async {
await wc.setFrame(frame);
}
}
+ if (lpos.isFullscreen == true) {
+ await restoreFrame();
+ // An duration is needed to avoid the window being restored after fullscreen.
+ Future.delayed(Duration(milliseconds: 300), () async {
+ stateGlobal.setFullscreen(true);
+ });
+ } else if (lpos.isMaximized == true) {
+ await restoreFrame();
+ // An duration is needed to avoid the window being restored after maximized.
+ Future.delayed(Duration(milliseconds: 300), () async {
+ await wc.maximize();
+ });
+ } else {
+ await restoreFrame();
+ }
break;
}
return false;
@@ -1538,7 +1741,7 @@ Future initUniLinks() async {
if (initialLink == null) {
return false;
}
- return parseRustdeskUri(initialLink);
+ return handleUriLink(uriString: initialLink);
} catch (err) {
debugPrintStack(label: "$err");
return false;
@@ -1556,10 +1759,10 @@ StreamSubscription? listenUniLinks({handleByFlutter = true}) {
}
final sub = uriLinkStream.listen((Uri? uri) {
- debugPrint("A uri was received: $uri.");
+ debugPrint("A uri was received: $uri. handleByFlutter $handleByFlutter");
if (uri != null) {
if (handleByFlutter) {
- callUniLinksUriHandler(uri);
+ handleUriLink(uri: uri);
} else {
bind.sendUrlScheme(url: uri.toString());
}
@@ -1572,97 +1775,166 @@ StreamSubscription? listenUniLinks({handleByFlutter = true}) {
return sub;
}
-/// Handle command line arguments
-///
-/// * Returns true if we successfully handle the startup arguments.
-bool checkArguments() {
- if (kBootArgs.isNotEmpty) {
- final ret = parseRustdeskUri(kBootArgs.first);
- if (ret) {
- return true;
+enum UriLinkType {
+ remoteDesktop,
+ fileTransfer,
+ portForward,
+ rdp,
+}
+
+// uri link handler
+bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) {
+ List? args;
+ if (cmdArgs != null) {
+ args = cmdArgs;
+ // rustdesk
+ if (args.isNotEmpty && args[0].startsWith(kUniLinksPrefix)) {
+ final uri = Uri.tryParse(args[0]);
+ if (uri != null) {
+ args = urlLinkToCmdArgs(uri);
+ }
+ }
+ } else if (uri != null) {
+ args = urlLinkToCmdArgs(uri);
+ } else if (uriString != null) {
+ final uri = Uri.tryParse(uriString);
+ if (uri != null) {
+ args = urlLinkToCmdArgs(uri);
}
}
- // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05]
- // check connect args
- var connectIndex = kBootArgs.indexOf("--connect");
- if (connectIndex == -1) {
+ if (args == null) {
return false;
}
- String? id =
- kBootArgs.length <= connectIndex + 1 ? null : kBootArgs[connectIndex + 1];
- String? password =
- kBootArgs.length <= connectIndex + 2 ? null : kBootArgs[connectIndex + 2];
- if (password != null && password.startsWith("--")) {
- password = null;
+
+ if (args.isEmpty) {
+ windowOnTop(null);
+ return true;
}
- final switchUuidIndex = kBootArgs.indexOf("--switch_uuid");
- String? switchUuid = kBootArgs.length <= switchUuidIndex + 1
- ? null
- : kBootArgs[switchUuidIndex + 1];
- if (id != null) {
- if (id.startsWith(kUniLinksPrefix)) {
- return parseRustdeskUri(id);
- } else {
- // remove "--connect xxx" in the `bootArgs` array
- kBootArgs.removeAt(connectIndex);
- kBootArgs.removeAt(connectIndex);
- // fallback to peer id
- Future.delayed(Duration.zero, () {
- rustDeskWinManager.newRemoteDesktop(id,
- password: password, switch_uuid: switchUuid);
- });
- return true;
+
+ UriLinkType? type;
+ String? id;
+ String? password;
+ String? switchUuid;
+ bool? forceRelay;
+ for (int i = 0; i < args.length; i++) {
+ switch (args[i]) {
+ case '--connect':
+ case '--play':
+ type = UriLinkType.remoteDesktop;
+ id = args[i + 1];
+ i++;
+ break;
+ case '--file-transfer':
+ type = UriLinkType.fileTransfer;
+ id = args[i + 1];
+ i++;
+ break;
+ case '--port-forward':
+ type = UriLinkType.portForward;
+ id = args[i + 1];
+ i++;
+ break;
+ case '--rdp':
+ type = UriLinkType.rdp;
+ id = args[i + 1];
+ i++;
+ break;
+ case '--password':
+ password = args[i + 1];
+ i++;
+ break;
+ case '--switch_uuid':
+ switchUuid = args[i + 1];
+ i++;
+ break;
+ case '--relay':
+ forceRelay = true;
+ break;
+ default:
+ break;
}
}
+ if (type != null && id != null) {
+ switch (type) {
+ case UriLinkType.remoteDesktop:
+ Future.delayed(Duration.zero, () {
+ rustDeskWinManager.newRemoteDesktop(id!,
+ password: password,
+ switchUuid: switchUuid,
+ forceRelay: forceRelay);
+ });
+ break;
+ case UriLinkType.fileTransfer:
+ Future.delayed(Duration.zero, () {
+ rustDeskWinManager.newFileTransfer(id!,
+ password: password, forceRelay: forceRelay);
+ });
+ break;
+ case UriLinkType.portForward:
+ Future.delayed(Duration.zero, () {
+ rustDeskWinManager.newPortForward(id!, false,
+ password: password, forceRelay: forceRelay);
+ });
+ break;
+ case UriLinkType.rdp:
+ Future.delayed(Duration.zero, () {
+ rustDeskWinManager.newPortForward(id!, true,
+ password: password, forceRelay: forceRelay);
+ });
+ break;
+ }
+
+ return true;
+ }
+
return false;
}
-/// Parse `rustdesk://` unilinks
-///
-/// Returns true if we successfully handle the uri provided.
-/// [Functions]
-/// 1. New Connection: rustdesk://connection/new/your_peer_id
-bool parseRustdeskUri(String uriPath) {
- final uri = Uri.tryParse(uriPath);
- if (uri == null) {
- debugPrint("uri is not valid: $uriPath");
- return false;
- }
- return callUniLinksUriHandler(uri);
-}
-
-/// uri handler
-///
-/// Returns true if we successfully handle the uri provided.
-bool callUniLinksUriHandler(Uri uri) {
- debugPrint("uni links called: $uri");
- // new connection
- String peerId;
- if (uri.authority == "connection" && uri.path.startsWith("/new/")) {
- peerId = uri.path.substring("/new/".length);
- } else if (uri.authority == "connect") {
- peerId = uri.path.substring(1);
+List? urlLinkToCmdArgs(Uri uri) {
+ String? command;
+ String? id;
+ if (uri.authority.isEmpty &&
+ uri.path.split('').every((char) => char == '/')) {
+ return [];
+ } else if (uri.authority == "connection" && uri.path.startsWith("/new/")) {
+ // For compatibility
+ command = '--connect';
+ id = uri.path.substring("/new/".length);
+ } else if (['connect', "play", 'file-transfer', 'port-forward', 'rdp']
+ .contains(uri.authority)) {
+ command = '--${uri.authority}';
+ if (uri.path.length > 1) {
+ id = uri.path.substring(1);
+ }
} else if (uri.authority.length > 2 && uri.path.length <= 1) {
- // "/" or ""
- peerId = uri.authority;
- } else {
- return false;
+ // rustdesk://
+ command = '--connect';
+ id = uri.authority;
}
- var param = uri.queryParameters;
- String? switch_uuid = param["switch_uuid"];
- String? password = param["password"];
- Future.delayed(Duration.zero, () {
- rustDeskWinManager.newRemoteDesktop(peerId,
- password: password, switch_uuid: switch_uuid);
- });
- return true;
+
+ List args = List.empty(growable: true);
+ if (command != null && id != null) {
+ args.add(command);
+ args.add(id);
+ var param = uri.queryParameters;
+ String? password = param["password"];
+ if (password != null) args.addAll(['--password', password]);
+ String? switch_uuid = param["switch_uuid"];
+ if (switch_uuid != null) args.addAll(['--switch_uuid', switch_uuid]);
+ if (param["relay"] != null) args.add("--relay");
+ return args;
+ }
+
+ return null;
}
-connectMainDesktop(String id,
- {required bool isFileTransfer,
- required bool isTcpTunneling,
- required bool isRDP,
- bool? forceRelay}) async {
+connectMainDesktop(
+ String id, {
+ required bool isFileTransfer,
+ required bool isTcpTunneling,
+ required bool isRDP,
+ bool? forceRelay,
+}) async {
if (isFileTransfer) {
await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay);
} else if (isTcpTunneling || isRDP) {
@@ -1676,11 +1948,22 @@ connectMainDesktop(String id,
/// If [isFileTransfer], starts a session only for file transfer.
/// If [isTcpTunneling], starts a session only for tcp tunneling.
/// If [isRDP], starts a session only for rdp.
-connect(BuildContext context, String id,
- {bool isFileTransfer = false,
- bool isTcpTunneling = false,
- bool isRDP = false}) async {
+connect(
+ BuildContext context,
+ String id, {
+ bool isFileTransfer = false,
+ bool isTcpTunneling = false,
+ bool isRDP = false,
+}) async {
if (id == '') return;
+ if (!isDesktop || desktopType == DesktopType.main) {
+ try {
+ if (Get.isRegistered()) {
+ final idController = Get.find();
+ idController.text = formatID(id);
+ }
+ } catch (_) {}
+ }
id = id.replaceAll(' ', '');
final oldId = id;
id = await bind.mainHandleRelayId(id: id);
@@ -1690,18 +1973,20 @@ connect(BuildContext context, String id,
if (isDesktop) {
if (desktopType == DesktopType.main) {
- await connectMainDesktop(id,
- isFileTransfer: isFileTransfer,
- isTcpTunneling: isTcpTunneling,
- isRDP: isRDP,
- forceRelay: forceRelay);
+ await connectMainDesktop(
+ id,
+ isFileTransfer: isFileTransfer,
+ isTcpTunneling: isTcpTunneling,
+ isRDP: isRDP,
+ forceRelay: forceRelay,
+ );
} else {
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
'id': id,
'isFileTransfer': isFileTransfer,
'isTcpTunneling': isTcpTunneling,
'isRDP': isRDP,
- "forceRelay": forceRelay,
+ 'forceRelay': forceRelay,
});
}
} else {
@@ -1790,10 +2075,14 @@ Future onActiveWindowChanged() async {
if (rustDeskWinManager.getActiveWindows().isEmpty) {
// close all sub windows
try {
- await Future.wait([
- saveWindowPosition(WindowType.Main),
- rustDeskWinManager.closeAllSubWindows()
- ]);
+ if (Platform.isLinux) {
+ await Future.wait([
+ saveWindowPosition(WindowType.Main),
+ rustDeskWinManager.closeAllSubWindows()
+ ]);
+ } else {
+ await rustDeskWinManager.closeAllSubWindows();
+ }
} catch (err) {
debugPrintStack(label: "$err");
} finally {
@@ -2093,11 +2382,87 @@ void onCopyFingerprint(String value) {
}
}
+Future callMainCheckSuperUserPermission() async {
+ bool checked = await bind.mainCheckSuperUserPermission();
+ if (Platform.isMacOS) {
+ await windowManager.show();
+ }
+ return checked;
+}
+
Future start_service(bool is_start) async {
bool checked = !bind.mainIsInstalled() ||
!Platform.isMacOS ||
- await bind.mainCheckSuperUserPermission();
+ await callMainCheckSuperUserPermission();
if (checked) {
bind.mainSetOption(key: "stop-service", value: is_start ? "" : "Y");
}
}
+
+typedef Future WhetherUseRemoteBlock();
+Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) {
+ var block = false.obs;
+ return Obx(() => MouseRegion(
+ onEnter: (_) async {
+ if (use != null && !await use()) {
+ block.value = false;
+ return;
+ }
+ var time0 = DateTime.now().millisecondsSinceEpoch;
+ await bind.mainCheckMouseTime();
+ Timer(const Duration(milliseconds: 120), () async {
+ var d = time0 - await bind.mainGetMouseTime();
+ if (d < 120) {
+ block.value = true;
+ }
+ });
+ },
+ onExit: (event) => block.value = false,
+ child: Stack(children: [
+ child,
+ Offstage(
+ offstage: !block.value,
+ child: Container(
+ color: Colors.black.withOpacity(0.5),
+ )),
+ ]),
+ ));
+}
+
+Widget unreadMessageCountBuilder(RxInt? count,
+ {double? size, double? fontSize}) {
+ return Obx(() => Offstage(
+ offstage: !((count?.value ?? 0) > 0),
+ child: Container(
+ width: size ?? 16,
+ height: size ?? 16,
+ decoration: BoxDecoration(
+ color: Colors.red,
+ shape: BoxShape.circle,
+ ),
+ child: Center(
+ child: Text("${count?.value ?? 0}",
+ maxLines: 1,
+ style: TextStyle(color: Colors.white, fontSize: fontSize ?? 10)),
+ ),
+ )));
+}
+
+Widget unreadTopRightBuilder(RxInt? count, {Widget? icon}) {
+ return Stack(
+ children: [
+ icon ?? Icon(Icons.chat),
+ Positioned(
+ top: 0,
+ right: 0,
+ child: unreadMessageCountBuilder(count, size: 12, fontSize: 8))
+ ],
+ );
+}
+
+String toCapitalized(String s) {
+ if (s.isEmpty) {
+ return s;
+ }
+ return s.substring(0, 1).toUpperCase() + s.substring(1);
+}
diff --git a/flutter/lib/common/formatter/id_formatter.dart b/flutter/lib/common/formatter/id_formatter.dart
index a9e4893a6..c2329d53f 100644
--- a/flutter/lib/common/formatter/id_formatter.dart
+++ b/flutter/lib/common/formatter/id_formatter.dart
@@ -26,6 +26,8 @@ class IDTextInputFormatter extends TextInputFormatter {
selection: TextSelection.collapsed(
offset: newID.length - selectionIndexFromTheRight,
),
+ // https://github.com/flutter/flutter/issues/78066#issuecomment-797869906
+ composing: newValue.composing,
);
}
}
@@ -33,6 +35,11 @@ class IDTextInputFormatter extends TextInputFormatter {
String formatID(String id) {
String id2 = id.replaceAll(' ', '');
+ String suffix = '';
+ if (id2.endsWith(r'\r') || id2.endsWith(r'/r')) {
+ suffix = id2.substring(id2.length - 2, id2.length);
+ id2 = id2.substring(0, id2.length - 2);
+ }
if (int.tryParse(id2) == null) return id;
String newID = '';
if (id2.length <= 3) {
@@ -45,7 +52,7 @@ String formatID(String id) {
newID += " ${id2.substring(i, i + 3)}";
}
}
- return newID;
+ return newID + suffix;
}
String trimID(String id) {
diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart
index d102d9f02..8e5c2d02a 100644
--- a/flutter/lib/common/hbbs/hbbs.dart
+++ b/flutter/lib/common/hbbs/hbbs.dart
@@ -1,4 +1,5 @@
-import 'dart:io';
+import 'dart:convert';
+import 'package:flutter/material.dart';
import 'package:flutter_hbb/models/peer_model.dart';
@@ -70,16 +71,6 @@ class PeerPayload {
}
}
-class DeviceInfo {
- static Map toJson() {
- final Map data = {};
- data['os'] = Platform.operatingSystem;
- data['type'] = "client";
- data['name'] = bind.mainGetHostname();
- return data;
- }
-}
-
class LoginRequest {
String? username;
String? password;
@@ -88,7 +79,6 @@ class LoginRequest {
bool? autoLogin;
String? type;
String? verificationCode;
- Map deviceInfo = DeviceInfo.toJson();
LoginRequest(
{this.username,
@@ -110,6 +100,13 @@ class LoginRequest {
if (verificationCode != null) {
data['verificationCode'] = verificationCode;
}
+
+ Map deviceInfo = {};
+ try {
+ deviceInfo = jsonDecode(bind.mainGetLoginDeviceInfo());
+ } catch (e) {
+ debugPrint('Failed to decode get device info: $e');
+ }
data['deviceInfo'] = deviceInfo;
return data;
}
diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart
index e4711ddf8..d5ce29190 100644
--- a/flutter/lib/common/shared_state.dart
+++ b/flutter/lib/common/shared_state.dart
@@ -285,6 +285,29 @@ class PeerStringOption {
Get.find(tag: tag(id, opt));
}
+class UnreadChatCountState {
+ static String tag(id) => 'unread_chat_count_$id';
+
+ static void init(String id) {
+ final key = tag(id);
+ if (!Get.isRegistered(tag: key)) {
+ final RxInt state = RxInt(0);
+ Get.put(state, tag: key);
+ } else {
+ Get.find(tag: key).value = 0;
+ }
+ }
+
+ static void delete(String id) {
+ final key = tag(id);
+ if (Get.isRegistered(tag: key)) {
+ Get.delete(tag: key);
+ }
+ }
+
+ static RxInt find(String id) => Get.find(tag: tag(id));
+}
+
initSharedStates(String id) {
PrivacyModeState.init(id);
BlockInputState.init(id);
@@ -294,6 +317,7 @@ initSharedStates(String id) {
RemoteCursorMovedState.init(id);
FingerprintState.init(id);
PeerBoolOption.init(id, 'zoom-cursor', () => false);
+ UnreadChatCountState.init(id);
}
removeSharedStates(String id) {
@@ -305,4 +329,5 @@ removeSharedStates(String id) {
RemoteCursorMovedState.delete(id);
FingerprintState.delete(id);
PeerBoolOption.delete(id, 'zoom-cursor');
+ UnreadChatCountState.delete(id);
}
diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart
index 58acc9ace..4af74e319 100644
--- a/flutter/lib/common/widgets/address_book.dart
+++ b/flutter/lib/common/widgets/address_book.dart
@@ -3,11 +3,14 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
-import '../../consts.dart';
+import 'package:flutter_hbb/models/ab_model.dart';
+import 'package:flutter_hbb/models/platform_model.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart';
+import 'package:flex_color_picker/flex_color_picker.dart';
import '../../common.dart';
+import 'dialog.dart';
import 'login.dart';
final hideAbTagsPanel = false.obs;
@@ -37,63 +40,115 @@ class _AddressBookState extends State {
child: ElevatedButton(
onPressed: loginDialog, child: Text(translate("Login"))));
} else {
- if (gFFI.abModel.abLoading.value) {
+ if (gFFI.abModel.abLoading.value && gFFI.abModel.emtpy) {
return const Center(
child: CircularProgressIndicator(),
);
}
- if (gFFI.abModel.abError.isNotEmpty) {
- return _buildShowError(gFFI.abModel.abError.value);
- }
- return isDesktop
- ? _buildAddressBookDesktop()
- : _buildAddressBookMobile();
+ return Column(
+ children: [
+ // NOT use Offstage to wrap LinearProgressIndicator
+ if (gFFI.abModel.retrying.value) LinearProgressIndicator(),
+ _buildErrorBanner(
+ err: gFFI.abModel.pullError,
+ retry: null,
+ close: () => gFFI.abModel.pullError.value = ''),
+ _buildErrorBanner(
+ err: gFFI.abModel.pushError,
+ retry: () => gFFI.abModel.pushAb(isRetry: true),
+ close: () => gFFI.abModel.pushError.value = ''),
+ Expanded(
+ child: isDesktop
+ ? _buildAddressBookDesktop()
+ : _buildAddressBookMobile())
+ ],
+ );
}
});
- Widget _buildShowError(String error) {
- return Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Text(translate(error)),
- TextButton(
- onPressed: () {
- gFFI.abModel.pullAb();
- },
- child: Text(translate("Retry")))
- ],
- ));
+ Widget _buildErrorBanner(
+ {required RxString err,
+ required Function? retry,
+ required Function close}) {
+ const double height = 25;
+ return Obx(() => Offstage(
+ offstage: !(!gFFI.abModel.abLoading.value && err.value.isNotEmpty),
+ child: Center(
+ child: Container(
+ height: height,
+ color: Color.fromARGB(255, 253, 238, 235),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ FittedBox(
+ child: Icon(
+ Icons.info,
+ color: Color.fromARGB(255, 249, 81, 81),
+ ),
+ ).marginAll(4),
+ Flexible(
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: Tooltip(
+ message: translate(err.value),
+ child: Text(
+ translate(err.value),
+ overflow: TextOverflow.ellipsis,
+ ),
+ )).marginSymmetric(vertical: 2),
+ ),
+ if (retry != null)
+ InkWell(
+ onTap: () {
+ retry.call();
+ },
+ child: Text(
+ translate("Retry"),
+ style: TextStyle(color: MyTheme.accent),
+ )).marginSymmetric(horizontal: 5),
+ FittedBox(
+ child: InkWell(
+ onTap: () {
+ close.call();
+ },
+ child: Icon(Icons.close).marginSymmetric(horizontal: 5),
+ ),
+ ).marginAll(4)
+ ],
+ ),
+ )).marginOnly(bottom: 14),
+ ));
}
Widget _buildAddressBookDesktop() {
return Row(
children: [
Offstage(
- offstage: hideAbTagsPanel.value,
- child: Container(
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(12),
- border:
- Border.all(color: Theme.of(context).colorScheme.background)),
- child: Container(
- width: 150,
- height: double.infinity,
- padding: const EdgeInsets.all(8.0),
- child: Column(
- children: [
- _buildTagHeader().marginOnly(left: 8.0, right: 0),
- Expanded(
- child: Container(
- width: double.infinity,
- height: double.infinity,
- child: _buildTags(),
- ),
- )
- ],
- ),
- ),
- ).marginOnly(right: 12.0)),
+ offstage: hideAbTagsPanel.value,
+ child: Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: Theme.of(context).colorScheme.background)),
+ child: Container(
+ width: 150,
+ height: double.infinity,
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ children: [
+ _buildTagHeader().marginOnly(left: 8.0, right: 0),
+ Expanded(
+ child: Container(
+ width: double.infinity,
+ height: double.infinity,
+ child: _buildTags(),
+ ),
+ )
+ ],
+ ),
+ ),
+ ).marginOnly(right: 12.0)),
_buildPeersViews()
],
);
@@ -102,25 +157,27 @@ class _AddressBookState extends State {
Widget _buildAddressBookMobile() {
return Column(
children: [
- Container(
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(6),
- border:
- Border.all(color: Theme.of(context).colorScheme.background)),
- child: Container(
- padding: const EdgeInsets.all(8.0),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- _buildTagHeader().marginOnly(left: 8.0, right: 0),
- Container(
- width: double.infinity,
- child: _buildTags(),
+ Offstage(
+ offstage: hideAbTagsPanel.value,
+ child: Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(6),
+ border: Border.all(
+ color: Theme.of(context).colorScheme.background)),
+ child: Container(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ _buildTagHeader().marginOnly(left: 8.0, right: 0),
+ Container(
+ width: double.infinity,
+ child: _buildTags(),
+ ),
+ ],
),
- ],
- ),
- ),
- ).marginOnly(bottom: 12.0),
+ ),
+ ).marginOnly(bottom: 12.0)),
_buildPeersViews()
],
);
@@ -144,9 +201,16 @@ class _AddressBookState extends State {
}
Widget _buildTags() {
- return Obx(
- () => Wrap(
- children: gFFI.abModel.tags
+ return Obx(() {
+ final List tags;
+ if (gFFI.abModel.sortTags.value) {
+ tags = gFFI.abModel.tags.toList();
+ tags.sort();
+ } else {
+ tags = gFFI.abModel.tags;
+ }
+ return Wrap(
+ children: tags
.map((e) => AddressBookTag(
name: e,
tags: gFFI.abModel.selectedTags,
@@ -158,8 +222,8 @@ class _AddressBookState extends State {
}
}))
.toList(),
- ),
- );
+ );
+ });
}
Widget _buildPeersViews() {
@@ -174,11 +238,44 @@ class _AddressBookState extends State {
);
}
+ @protected
+ MenuEntryBase syncMenuItem() {
+ return MenuEntrySwitch(
+ switchType: SwitchType.scheckbox,
+ text: translate('Sync with recent sessions'),
+ getter: () async {
+ return shouldSyncAb();
+ },
+ setter: (bool v) async {
+ bind.mainSetLocalOption(key: syncAbOption, value: v ? 'Y' : '');
+ },
+ dismissOnClicked: true,
+ );
+ }
+
+ @protected
+ MenuEntryBase sortMenuItem() {
+ return MenuEntrySwitch(
+ switchType: SwitchType.scheckbox,
+ text: translate('Sort tags'),
+ getter: () async {
+ return shouldSortTags();
+ },
+ setter: (bool v) async {
+ bind.mainSetLocalOption(key: sortAbTagsOption, value: v ? 'Y' : '');
+ gFFI.abModel.sortTags.value = v;
+ },
+ dismissOnClicked: true,
+ );
+ }
+
void _showMenu(RelativeRect pos) {
final items = [
getEntry(translate("Add ID"), abAddId),
getEntry(translate("Add Tag"), abAddTag),
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
+ sortMenuItem(),
+ syncMenuItem(),
];
mod_menu.showMenu(
@@ -198,6 +295,9 @@ class _AddressBookState extends State {
}
void abAddId() async {
+ if (gFFI.abModel.isFull(true)) {
+ return;
+ }
var isInProgress = false;
IDTextEditingController idController = IDTextEditingController(text: '');
TextEditingController aliasController = TextEditingController(text: '');
@@ -224,13 +324,14 @@ class _AddressBookState extends State {
return;
}
gFFI.abModel.addId(id, aliasController.text.trim(), selectedTag);
- await gFFI.abModel.pushAb();
+ gFFI.abModel.pushAb();
this.setState(() {});
// final currentPeers
}
close();
}
+ double marginBottom = 4;
return CustomAlertDialog(
title: Text(translate("Add ID")),
content: Column(
@@ -252,7 +353,7 @@ class _AddressBookState extends State {
),
],
),
- ),
+ ).marginOnly(bottom: marginBottom),
TextField(
controller: idController,
inputFormatters: [IDTextInputFormatter()],
@@ -264,7 +365,7 @@ class _AddressBookState extends State {
translate('Alias'),
style: style,
),
- ).marginOnly(top: 8, bottom: 2),
+ ).marginOnly(top: 8, bottom: marginBottom),
TextField(
controller: aliasController,
),
@@ -274,8 +375,9 @@ class _AddressBookState extends State {
translate('Tags'),
style: style,
),
- ).marginOnly(top: 8),
- Container(
+ ).marginOnly(top: 8, bottom: marginBottom),
+ Align(
+ alignment: Alignment.centerLeft,
child: Wrap(
children: tags
.map((e) => AddressBookTag(
@@ -297,8 +399,8 @@ class _AddressBookState extends State {
const SizedBox(
height: 4.0,
),
- Offstage(
- offstage: !isInProgress, child: const LinearProgressIndicator())
+ // NOT use Offstage to wrap LinearProgressIndicator
+ if (isInProgress) const LinearProgressIndicator(),
],
),
actions: [
@@ -331,7 +433,7 @@ class _AddressBookState extends State {
for (final tag in tags) {
gFFI.abModel.addTag(tag);
}
- await gFFI.abModel.pushAb();
+ gFFI.abModel.pushAb();
// final currentPeers
}
close();
@@ -363,8 +465,8 @@ class _AddressBookState extends State {
const SizedBox(
height: 4.0,
),
- Offstage(
- offstage: !isInProgress, child: const LinearProgressIndicator())
+ // NOT use Offstage to wrap LinearProgressIndicator
+ if (isInProgress) const LinearProgressIndicator(),
],
),
actions: [
@@ -402,31 +504,95 @@ class AddressBookTag extends StatelessWidget {
pos = RelativeRect.fromLTRB(x, y, x, y);
}
+ const double radius = 8;
return GestureDetector(
onTap: onTap,
onTapDown: showActionMenu ? setPosition : null,
onSecondaryTapDown: showActionMenu ? setPosition : null,
onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null,
onLongPress: showActionMenu ? () => _showMenu(context, pos) : null,
- child: Obx(
- () => Container(
- decoration: BoxDecoration(
- color: tags.contains(name)
- ? Colors.blue
- : Theme.of(context).colorScheme.background,
- borderRadius: BorderRadius.circular(6)),
- margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0),
- padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0),
- child: Text(name,
- style:
- TextStyle(color: tags.contains(name) ? Colors.white : null)),
- ),
- ),
+ child: Obx(() => Container(
+ decoration: BoxDecoration(
+ color: tags.contains(name)
+ ? gFFI.abModel.getTagColor(name)
+ : Theme.of(context).colorScheme.background,
+ borderRadius: BorderRadius.circular(4)),
+ margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
+ padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 6.0),
+ child: IntrinsicWidth(
+ child: Row(
+ children: [
+ Container(
+ width: radius,
+ height: radius,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: tags.contains(name)
+ ? Colors.white
+ : gFFI.abModel.getTagColor(name)),
+ ).marginOnly(right: radius / 2),
+ Expanded(
+ child: Text(name,
+ style: TextStyle(
+ overflow: TextOverflow.ellipsis,
+ color: tags.contains(name) ? Colors.white : null)),
+ ),
+ ],
+ ),
+ ),
+ )),
);
}
void _showMenu(BuildContext context, RelativeRect pos) {
final items = [
+ getEntry(translate("Rename"), () {
+ renameDialog(
+ oldName: name,
+ validator: (String? newName) {
+ if (newName == null || newName.isEmpty) {
+ return translate('Can not be empty');
+ }
+ if (newName != name && gFFI.abModel.tags.contains(newName)) {
+ return translate('Already exists');
+ }
+ return null;
+ },
+ onSubmit: (String newName) {
+ if (name != newName) {
+ gFFI.abModel.renameTag(name, newName);
+ gFFI.abModel.pushAb();
+ }
+ Future.delayed(Duration.zero, () => Get.back());
+ },
+ onCancel: () {
+ Future.delayed(Duration.zero, () => Get.back());
+ });
+ }),
+ getEntry(translate(translate('Change Color')), () async {
+ final model = gFFI.abModel;
+ Color oldColor = model.getTagColor(name);
+ Color newColor = await showColorPickerDialog(
+ context,
+ oldColor,
+ pickersEnabled: {
+ ColorPickerType.accent: false,
+ ColorPickerType.wheel: true,
+ },
+ pickerTypeLabels: {
+ ColorPickerType.primary: translate("Primary Color"),
+ ColorPickerType.wheel: translate("HSV Color"),
+ },
+ actionButtons: ColorPickerActionButtons(
+ dialogOkButtonLabel: translate("OK"),
+ dialogCancelButtonLabel: translate("Cancel")),
+ showColorCode: true,
+ );
+ if (oldColor != newColor) {
+ model.setTagColor(name, newColor);
+ model.pushAb();
+ }
+ }),
getEntry(translate("Delete"), () {
gFFI.abModel.deleteTag(name);
gFFI.abModel.pushAb();
@@ -458,7 +624,6 @@ MenuEntryButton getEntry(String title, VoidCallback proc) {
style: style,
),
proc: proc,
- padding: kDesktopMenuPadding,
dismissOnClicked: true,
);
}
diff --git a/flutter/lib/common/widgets/animated_rotation_widget.dart b/flutter/lib/common/widgets/animated_rotation_widget.dart
index 525508e44..0efc71552 100644
--- a/flutter/lib/common/widgets/animated_rotation_widget.dart
+++ b/flutter/lib/common/widgets/animated_rotation_widget.dart
@@ -1,11 +1,17 @@
import 'package:flutter/material.dart';
+import 'package:get/get.dart';
class AnimatedRotationWidget extends StatefulWidget {
final VoidCallback onPressed;
final ValueChanged? onHover;
final Widget child;
+ final RxBool? spinning;
const AnimatedRotationWidget(
- {super.key, required this.onPressed, required this.child, this.onHover});
+ {super.key,
+ required this.onPressed,
+ required this.child,
+ this.spinning,
+ this.onHover});
@override
State createState() => AnimatedRotationWidgetState();
@@ -14,14 +20,31 @@ class AnimatedRotationWidget extends StatefulWidget {
class AnimatedRotationWidgetState extends State {
double turns = 0.0;
+ @override
+ void initState() {
+ super.initState();
+ widget.spinning?.listen((v) {
+ if (v && mounted) {
+ setState(() {
+ turns += 1;
+ });
+ }
+ });
+ }
+
@override
Widget build(BuildContext context) {
return AnimatedRotation(
turns: turns,
duration: const Duration(milliseconds: 200),
+ onEnd: () {
+ if (widget.spinning?.value == true && mounted) {
+ setState(() => turns += 1.0);
+ }
+ },
child: InkWell(
onTap: () {
- setState(() => turns += 1.0);
+ if (mounted) setState(() => turns += 1.0);
widget.onPressed();
},
onHover: widget.onHover,
diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart
index 0e6be569e..b6611d3ed 100644
--- a/flutter/lib/common/widgets/chat_page.dart
+++ b/flutter/lib/common/widgets/chat_page.dart
@@ -7,10 +7,16 @@ import 'package:provider/provider.dart';
import '../../mobile/pages/home_page.dart';
+enum ChatPageType {
+ mobileMain,
+ desktopCM,
+}
+
class ChatPage extends StatelessWidget implements PageShape {
late final ChatModel chatModel;
+ final ChatPageType? type;
- ChatPage({ChatModel? chatModel}) {
+ ChatPage({ChatModel? chatModel, this.type}) {
this.chatModel = chatModel ?? gFFI.chatModel;
}
@@ -18,27 +24,53 @@ class ChatPage extends StatelessWidget implements PageShape {
final title = translate("Chat");
@override
- final icon = Icon(Icons.chat);
+ final icon = unreadTopRightBuilder(gFFI.chatModel.mobileUnreadSum);
@override
final appBarActions = [
- PopupMenuButton(
+ PopupMenuButton(
tooltip: "",
- icon: Icon(Icons.group),
+ icon: unreadTopRightBuilder(gFFI.chatModel.mobileUnreadSum,
+ icon: Icon(Icons.group)),
itemBuilder: (context) {
// only mobile need [appBarActions], just bind gFFI.chatModel
final chatModel = gFFI.chatModel;
return chatModel.messages.entries.map((entry) {
- final id = entry.key;
+ final key = entry.key;
final user = entry.value.chatUser;
- return PopupMenuItem(
- child: Text("${user.firstName} ${user.id}"),
- value: id,
+ final client = gFFI.serverModel.clients
+ .firstWhereOrNull((e) => e.id == key.connId);
+ final connected =
+ gFFI.serverModel.clients.any((e) => e.id == key.connId);
+ return PopupMenuItem(
+ child: Row(
+ children: [
+ Icon(
+ key.isOut
+ ? Icons.call_made_rounded
+ : Icons.call_received_rounded,
+ color: MyTheme.accent)
+ .marginOnly(right: 6),
+ Text("${user.firstName} ${user.id}"),
+ if (connected)
+ Container(
+ width: 10,
+ height: 10,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Color.fromARGB(255, 46, 205, 139)),
+ ).marginSymmetric(horizontal: 2),
+ if (client != null)
+ unreadMessageCountBuilder(client.unreadChatMessageCount)
+ .marginOnly(left: 4)
+ ],
+ ),
+ value: key,
);
}).toList();
},
- onSelected: (id) {
- gFFI.chatModel.changeCurrentID(id);
+ onSelected: (key) {
+ gFFI.chatModel.changeCurrentKey(key);
})
];
@@ -50,16 +82,27 @@ class ChatPage extends StatelessWidget implements PageShape {
color: Theme.of(context).scaffoldBackgroundColor,
child: Consumer(
builder: (context, chatModel, child) {
- final currentUser = chatModel.currentUser;
+ final readOnly = type == ChatPageType.mobileMain &&
+ (chatModel.currentKey.connId == ChatModel.clientModeID ||
+ gFFI.serverModel.clients.every((e) =>
+ e.id != chatModel.currentKey.connId ||
+ chatModel.currentUser == null)) ||
+ type == ChatPageType.desktopCM &&
+ gFFI.serverModel.clients
+ .firstWhereOrNull(
+ (e) => e.id == chatModel.currentKey.connId)
+ ?.disconnected ==
+ true;
return Stack(
children: [
LayoutBuilder(builder: (context, constraints) {
final chat = DashChat(
onSend: chatModel.send,
currentUser: chatModel.me,
- messages:
- chatModel.messages[chatModel.currentID]?.chatMessages ??
- [],
+ messages: chatModel
+ .messages[chatModel.currentKey]?.chatMessages ??
+ [],
+ readOnly: readOnly,
inputOptions: InputOptions(
focusNode: chatModel.inputNode,
textController: chatModel.textController,
@@ -127,22 +170,6 @@ class ChatPage extends StatelessWidget implements PageShape {
);
return SelectionArea(child: chat);
}),
- desktopType == DesktopType.cm ||
- chatModel.currentID == ChatModel.clientModeID
- ? SizedBox.shrink()
- : Padding(
- padding: EdgeInsets.all(12),
- child: Row(
- children: [
- Icon(Icons.account_circle, color: MyTheme.accent80),
- SizedBox(width: 5),
- Text(
- "${currentUser.firstName} ${currentUser.id}",
- style: TextStyle(color: MyTheme.accent),
- ),
- ],
- ),
- ),
],
).paddingOnly(bottom: 8);
},
diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart
index 2e60304be..a2a4e2b23 100644
--- a/flutter/lib/common/widgets/dialog.dart
+++ b/flutter/lib/common/widgets/dialog.dart
@@ -1,14 +1,15 @@
import 'dart:async';
-import 'package:debounce_throttle/debounce_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/shared_state.dart';
+import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:get/get.dart';
import '../../common.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
+import 'address_book.dart';
void clientClose(SessionID sessionId, OverlayDialogManager dialogManager) {
msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
@@ -155,8 +156,8 @@ void changeIdDialog() {
}).toList(),
)).marginOnly(bottom: 8)
: SizedBox.shrink(),
- Offstage(
- offstage: !isInProgress, child: const LinearProgressIndicator())
+ // NOT use Offstage to wrap LinearProgressIndicator
+ if (isInProgress) const LinearProgressIndicator(),
],
),
actions: [
@@ -201,8 +202,8 @@ void changeWhiteList({Function()? callback}) async {
const SizedBox(
height: 4.0,
),
- Offstage(
- offstage: !isInProgress, child: const LinearProgressIndicator())
+ // NOT use Offstage to wrap LinearProgressIndicator
+ if (isInProgress) const LinearProgressIndicator(),
],
),
actions: [
@@ -811,6 +812,8 @@ void showRequestElevationDialog(
} else {
bind.sessionElevateDirect(sessionId: sessionId);
}
+ close();
+ showWaitUacDialog(sessionId, dialogManager, "wait-uac");
}
return CustomAlertDialog(
@@ -943,16 +946,23 @@ showSetOSPassword(
SessionID sessionId,
bool login,
OverlayDialogManager dialogManager,
+ String? osPassword,
+ Function()? closeCallback,
) async {
final controller = TextEditingController();
- var password =
+ osPassword ??=
await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
'';
var autoLogin =
await bind.sessionGetOption(sessionId: sessionId, arg: 'auto-login') !=
'';
- controller.text = password;
+ controller.text = osPassword;
dialogManager.show((setState, close, context) {
+ closeWithCallback([dynamic]) {
+ close();
+ if (closeCallback != null) closeCallback();
+ }
+
submit() {
var text = controller.text.trim();
bind.sessionPeerOption(
@@ -964,7 +974,7 @@ showSetOSPassword(
if (text != '' && login) {
bind.sessionInputOsPassword(sessionId: sessionId, value: text);
}
- close();
+ closeWithCallback();
}
return CustomAlertDialog(
@@ -998,7 +1008,7 @@ showSetOSPassword(
dialogButton(
"Cancel",
icon: Icon(Icons.close_rounded),
- onPressed: close,
+ onPressed: closeWithCallback,
isOutline: true,
),
dialogButton(
@@ -1008,7 +1018,7 @@ showSetOSPassword(
),
],
onSubmit: submit,
- onCancel: close,
+ onCancel: closeWithCallback,
);
});
}
@@ -1215,50 +1225,9 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId);
qualityInitValue =
quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0;
- const qualityMinValue = 10.0;
- const qualityMaxValue = 100.0;
- if (qualityInitValue < qualityMinValue) {
- qualityInitValue = qualityMinValue;
+ if (qualityInitValue < 10 || qualityInitValue > 2000) {
+ qualityInitValue = 50;
}
- if (qualityInitValue > qualityMaxValue) {
- qualityInitValue = qualityMaxValue;
- }
- final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
- final debouncerQuality = Debouncer(
- Duration(milliseconds: 1000),
- onChanged: (double v) {
- setCustomValues(quality: v);
- },
- initialValue: qualityInitValue,
- );
- final qualitySlider = Obx(() => Row(
- children: [
- Expanded(
- flex: 3,
- child: Slider(
- value: qualitySliderValue.value,
- min: qualityMinValue,
- max: qualityMaxValue,
- divisions: 18,
- onChanged: (double value) {
- qualitySliderValue.value = value;
- debouncerQuality.value = value;
- },
- )),
- Expanded(
- flex: 1,
- child: Text(
- '${qualitySliderValue.value.round()}%',
- style: const TextStyle(fontSize: 15),
- )),
- Expanded(
- flex: 2,
- child: Text(
- translate('Bitrate'),
- style: const TextStyle(fontSize: 15),
- )),
- ],
- ));
// fps
final fpsOption =
await bind.sessionGetOption(sessionId: sessionId, arg: 'custom-fps');
@@ -1266,54 +1235,187 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
if (fpsInitValue < 5 || fpsInitValue > 120) {
fpsInitValue = 30;
}
- final RxDouble fpsSliderValue = RxDouble(fpsInitValue);
- final debouncerFps = Debouncer(
- Duration(milliseconds: 1000),
- onChanged: (double v) {
- setCustomValues(fps: v);
- },
- initialValue: qualityInitValue,
- );
bool? direct;
try {
direct =
ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
} catch (_) {}
- final fpsSlider = Offstage(
- offstage: (await bind.mainIsUsingPublicServer() && direct != true) ||
- version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0,
- child: Row(
- children: [
- Expanded(
- flex: 3,
- child: Obx((() => Slider(
- value: fpsSliderValue.value,
- min: 5,
- max: 120,
- divisions: 23,
- onChanged: (double value) {
- fpsSliderValue.value = value;
- debouncerFps.value = value;
- },
- )))),
- Expanded(
- flex: 1,
- child: Obx(() => Text(
- '${fpsSliderValue.value.round()}',
- style: const TextStyle(fontSize: 15),
- ))),
- Expanded(
- flex: 2,
- child: Text(
- translate('FPS'),
- style: const TextStyle(fontSize: 15),
- ))
- ],
- ),
- );
+ bool notShowFps = (await bind.mainIsUsingPublicServer() && direct != true) ||
+ version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
- final content = Column(
- children: [qualitySlider, fpsSlider],
- );
+ final content = customImageQualityWidget(
+ initQuality: qualityInitValue,
+ initFps: fpsInitValue,
+ setQuality: (v) => setCustomValues(quality: v),
+ setFps: (v) => setCustomValues(fps: v),
+ showFps: !notShowFps);
msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
}
+
+void deletePeerConfirmDialog(Function onSubmit, String title) async {
+ gFFI.dialogManager.show(
+ (setState, close, context) {
+ submit() async {
+ await onSubmit();
+ close();
+ }
+
+ return CustomAlertDialog(
+ title: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.delete_rounded,
+ color: Colors.red,
+ ),
+ Expanded(
+ child: Text(title, overflow: TextOverflow.ellipsis).paddingOnly(
+ left: 10,
+ ),
+ ),
+ ],
+ ),
+ content: SizedBox.shrink(),
+ actions: [
+ dialogButton(
+ "Cancel",
+ icon: Icon(Icons.close_rounded),
+ onPressed: close,
+ isOutline: true,
+ ),
+ dialogButton(
+ "OK",
+ icon: Icon(Icons.done_rounded),
+ onPressed: submit,
+ ),
+ ],
+ onSubmit: submit,
+ onCancel: close,
+ );
+ },
+ );
+}
+
+void editAbTagDialog(
+ List currentTags, Function(List) onSubmit) {
+ var isInProgress = false;
+
+ final tags = List.of(gFFI.abModel.tags);
+ var selectedTag = currentTags.obs;
+
+ gFFI.dialogManager.show((setState, close, context) {
+ submit() async {
+ setState(() {
+ isInProgress = true;
+ });
+ await onSubmit(selectedTag);
+ close();
+ }
+
+ return CustomAlertDialog(
+ title: Text(translate("Edit Tag")),
+ content: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: Wrap(
+ children: tags
+ .map((e) => AddressBookTag(
+ name: e,
+ tags: selectedTag,
+ onTap: () {
+ if (selectedTag.contains(e)) {
+ selectedTag.remove(e);
+ } else {
+ selectedTag.add(e);
+ }
+ },
+ showActionMenu: false))
+ .toList(growable: false),
+ ),
+ ),
+ // NOT use Offstage to wrap LinearProgressIndicator
+ if (isInProgress) const LinearProgressIndicator(),
+ ],
+ ),
+ actions: [
+ dialogButton("Cancel", onPressed: close, isOutline: true),
+ dialogButton("OK", onPressed: submit),
+ ],
+ onSubmit: submit,
+ onCancel: close,
+ );
+ });
+}
+
+void renameDialog(
+ {required String oldName,
+ FormFieldValidator? validator,
+ required ValueChanged onSubmit,
+ Function? onCancel}) async {
+ RxBool isInProgress = false.obs;
+ var controller = TextEditingController(text: oldName);
+ final formKey = GlobalKey();
+ gFFI.dialogManager.show((setState, close, context) {
+ submit() async {
+ String text = controller.text.trim();
+ if (validator != null && formKey.currentState?.validate() == false) {
+ return;
+ }
+ isInProgress.value = true;
+ onSubmit(text);
+ close();
+ isInProgress.value = false;
+ }
+
+ cancel() {
+ onCancel?.call();
+ close();
+ }
+
+ return CustomAlertDialog(
+ title: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.edit_rounded, color: MyTheme.accent),
+ Text(translate('Rename')).paddingOnly(left: 10),
+ ],
+ ),
+ content: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ child: Form(
+ key: formKey,
+ child: TextFormField(
+ controller: controller,
+ autofocus: true,
+ decoration: InputDecoration(labelText: translate('Name')),
+ validator: validator,
+ ),
+ ),
+ ),
+ // NOT use Offstage to wrap LinearProgressIndicator
+ Obx(() =>
+ isInProgress.value ? const LinearProgressIndicator() : Offstage())
+ ],
+ ),
+ actions: [
+ dialogButton(
+ "Cancel",
+ icon: Icon(Icons.close_rounded),
+ onPressed: cancel,
+ isOutline: true,
+ ),
+ dialogButton(
+ "OK",
+ icon: Icon(Icons.done_rounded),
+ onPressed: submit,
+ ),
+ ],
+ onSubmit: submit,
+ onCancel: cancel,
+ );
+ });
+}
diff --git a/flutter/lib/mobile/widgets/gestures.dart b/flutter/lib/common/widgets/gestures.dart
similarity index 96%
rename from flutter/lib/mobile/widgets/gestures.dart
rename to flutter/lib/common/widgets/gestures.dart
index 77f9c42fd..aeff15041 100644
--- a/flutter/lib/mobile/widgets/gestures.dart
+++ b/flutter/lib/common/widgets/gestures.dart
@@ -113,13 +113,14 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
}
void onOneFingerStartDebounce(ScaleUpdateDetails d) {
- final start = (ScaleUpdateDetails d) {
+ start(ScaleUpdateDetails d) {
_currentState = GestureState.oneFingerPan;
if (onOneFingerPanStart != null) {
onOneFingerPanStart!(DragStartDetails(
localPosition: d.localFocalPoint, globalPosition: d.focalPoint));
}
- };
+ }
+
if (_currentState != GestureState.none) {
_debounceTimer = Timer(Duration(milliseconds: 200), () {
start(d);
@@ -132,13 +133,14 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
}
void onTwoFingerStartDebounce(ScaleUpdateDetails d) {
- final start = (ScaleUpdateDetails d) {
+ start(ScaleUpdateDetails d) {
_currentState = GestureState.twoFingerScale;
if (onTwoFingerScaleStart != null) {
onTwoFingerScaleStart!(ScaleStartDetails(
localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint));
}
- };
+ }
+
if (_currentState == GestureState.threeFingerVerticalDrag) {
_debounceTimer = Timer(Duration(milliseconds: 200), () {
start(d);
@@ -182,6 +184,8 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer {
_TapTracker? _firstTap;
_TapTracker? _secondTap;
+ PointerDownEvent? _lastPointerDownEvent;
+
final Map _trackers = {};
@override
@@ -236,6 +240,7 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer {
gestureSettings: gestureSettings,
);
_trackers[event.pointer] = tracker;
+ _lastPointerDownEvent = event;
tracker.startTrackingPointer(_handleEvent, event.transform);
}
@@ -246,7 +251,11 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer {
_registerFirstTap(tracker);
} else if (_secondTap != null) {
if (event.pointer == _secondTap!.pointer) {
- if (onHoldDragEnd != null) onHoldDragEnd!(DragEndDetails());
+ if (onHoldDragEnd != null) {
+ onHoldDragEnd!(DragEndDetails());
+ _secondTap = null;
+ _isStart = false;
+ }
}
} else {
_reject(tracker);
@@ -266,11 +275,12 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer {
if (!_isStart) {
_resolve();
}
- if (onHoldDragUpdate != null)
+ if (onHoldDragUpdate != null) {
onHoldDragUpdate!(DragUpdateDetails(
globalPosition: event.position,
localPosition: event.localPosition,
delta: event.delta));
+ }
}
}
} else if (event is PointerCancelEvent) {
@@ -300,7 +310,11 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer {
_secondTap?.entry.resolve(GestureDisposition.accepted);
_isStart = true;
// TODO start details
- if (onHoldDragStart != null) onHoldDragStart!(DragStartDetails());
+ if (onHoldDragStart != null) {
+ onHoldDragStart!(DragStartDetails(
+ kind: _lastPointerDownEvent?.kind,
+ ));
+ }
}
void _reject(_TapTracker tracker) {
@@ -432,6 +446,8 @@ class DoubleFinerTapGestureRecognizer extends GestureRecognizer {
Timer? _firstTapTimer;
_TapTracker? _firstTap;
+ PointerDownEvent? _lastPointerDownEvent;
+
var _isStart = false;
final Set _upTap = {};
@@ -473,6 +489,7 @@ class DoubleFinerTapGestureRecognizer extends GestureRecognizer {
} else {
// first tap
_isStart = true;
+ _lastPointerDownEvent = event;
_startFirstTapDownTimer();
}
_trackTap(event);
@@ -498,8 +515,9 @@ class DoubleFinerTapGestureRecognizer extends GestureRecognizer {
debugPrint("PointerUpEvent");
_upTap.add(tracker.pointer);
} else if (event is PointerMoveEvent) {
- if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop))
+ if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) {
_reject(tracker);
+ }
} else if (event is PointerCancelEvent) {
_reject(tracker);
}
@@ -587,7 +605,11 @@ class DoubleFinerTapGestureRecognizer extends GestureRecognizer {
void _resolve() {
// TODO tap down details
- if (onDoubleFinerTap != null) onDoubleFinerTap!(TapDownDetails());
+ if (onDoubleFinerTap != null) {
+ onDoubleFinerTap!(TapDownDetails(
+ kind: _lastPointerDownEvent?.kind,
+ ));
+ }
_trackers.forEach((key, value) {
value.entry.resolve(GestureDisposition.accepted);
});
diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart
index d5e2a9ba3..b26397b94 100644
--- a/flutter/lib/common/widgets/login.dart
+++ b/flutter/lib/common/widgets/login.dart
@@ -12,25 +12,43 @@ import 'package:url_launcher/url_launcher.dart';
import '../../common.dart';
import './dialog.dart';
+const kOpSvgList = [
+ 'github',
+ 'gitlab',
+ 'google',
+ 'apple',
+ 'okta',
+ 'facebook',
+ 'azure',
+ 'auth0'
+];
+
class _IconOP extends StatelessWidget {
- final String icon;
- final double iconWidth;
+ final String op;
+ final String? icon;
final EdgeInsets margin;
const _IconOP(
{Key? key,
+ required this.op,
required this.icon,
- required this.iconWidth,
this.margin = const EdgeInsets.symmetric(horizontal: 4.0)})
: super(key: key);
@override
Widget build(BuildContext context) {
+ final svgFile =
+ kOpSvgList.contains(op.toLowerCase()) ? op.toLowerCase() : 'default';
return Container(
margin: margin,
- child: SvgPicture.asset(
- 'assets/$icon.svg',
- width: iconWidth,
- ),
+ child: icon == null
+ ? SvgPicture.asset(
+ 'assets/auth-$svgFile.svg',
+ width: 20,
+ )
+ : SvgPicture.string(
+ icon!,
+ width: 20,
+ ),
);
}
}
@@ -38,7 +56,7 @@ class _IconOP extends StatelessWidget {
class ButtonOP extends StatelessWidget {
final String op;
final RxString curOP;
- final double iconWidth;
+ final String? icon;
final Color primaryColor;
final double height;
final Function() onTap;
@@ -47,7 +65,7 @@ class ButtonOP extends StatelessWidget {
Key? key,
required this.op,
required this.curOP,
- required this.iconWidth,
+ required this.icon,
required this.primaryColor,
required this.height,
required this.onTap,
@@ -55,13 +73,18 @@ class ButtonOP extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final opLabel = {
+ 'github': 'GitHub',
+ 'gitlab': 'GitLab'
+ }[op.toLowerCase()] ??
+ toCapitalized(op);
return Row(children: [
Container(
height: height,
width: 200,
child: Obx(() => ElevatedButton(
style: ElevatedButton.styleFrom(
- primary: curOP.value.isEmpty || curOP.value == op
+ backgroundColor: curOP.value.isEmpty || curOP.value == op
? primaryColor
: Colors.grey,
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
@@ -69,17 +92,20 @@ class ButtonOP extends StatelessWidget {
child: Row(
children: [
SizedBox(
- width: 30,
- child: _IconOP(
- icon: op,
- iconWidth: iconWidth,
- margin: EdgeInsets.only(right: 5),
- )),
+ width: 30,
+ child: _IconOP(
+ op: op,
+ icon: icon,
+ margin: EdgeInsets.only(right: 5),
+ ),
+ ),
Expanded(
- child: FittedBox(
- fit: BoxFit.scaleDown,
- child: Center(
- child: Text('${translate("Continue with")} $op')))),
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: Center(
+ child: Text('${translate("Continue with")} $opLabel')),
+ ),
+ ),
],
))),
),
@@ -89,8 +115,8 @@ class ButtonOP extends StatelessWidget {
class ConfigOP {
final String op;
- final double iconWidth;
- ConfigOP({required this.op, required this.iconWidth});
+ final String? icon;
+ ConfigOP({required this.op, required this.icon});
}
class WidgetOP extends StatefulWidget {
@@ -182,7 +208,7 @@ class _WidgetOPState extends State {
ButtonOP(
op: widget.config.op,
curOP: widget.curOP,
- iconWidth: widget.config.iconWidth,
+ icon: widget.config.icon,
primaryColor: str2color(widget.config.op, 0x7f),
height: 36,
onTap: () async {
@@ -333,9 +359,8 @@ class LoginWidgetUserPass extends StatelessWidget {
autoFocus: false,
errorText: passMsg,
),
- Offstage(
- offstage: !isInProgress,
- child: const LinearProgressIndicator()),
+ // NOT use Offstage to wrap LinearProgressIndicator
+ if (isInProgress) const LinearProgressIndicator(),
const SizedBox(height: 12.0),
FittedBox(
child:
@@ -380,7 +405,7 @@ Future loginDialog() async {
final loginOptions = [].obs;
Future.delayed(Duration.zero, () async {
- loginOptions.value = await UserModel.queryLoginOptions();
+ loginOptions.value = await UserModel.queryOidcLoginOptions();
});
final res = await gFFI.dialogManager.show((setState, close, context) {
@@ -434,11 +459,16 @@ Future loginDialog() async {
}
break;
case HttpType.kAuthResTypeEmailCheck:
- setState(() => isInProgress = false);
- final res = await verificationCodeDialog(resp.user);
- if (res == true) {
+ if (isMobile) {
close(true);
- return;
+ verificationCodeDialog(resp.user);
+ } else {
+ setState(() => isInProgress = false);
+ final res = await verificationCodeDialog(resp.user);
+ if (res == true) {
+ close(true);
+ return;
+ }
}
break;
default:
@@ -455,12 +485,8 @@ Future loginDialog() async {
}
thirdAuthWidget() => Obx(() {
- final oidcOptions = loginOptions
- .where((opt) => opt.startsWith(kAuthReqTypeOidc))
- .map((opt) => opt.substring(kAuthReqTypeOidc.length))
- .toList();
return Offstage(
- offstage: oidcOptions.isEmpty,
+ offstage: loginOptions.isEmpty,
child: Column(
children: [
const SizedBox(
@@ -475,12 +501,8 @@ Future loginDialog() async {
height: 8.0,
),
LoginWidgetOP(
- ops: [
- ConfigOP(op: 'GitHub', iconWidth: 20),
- ConfigOP(op: 'Google', iconWidth: 20),
- ConfigOP(op: 'Okta', iconWidth: 38),
- ]
- .where((op) => oidcOptions.contains(op.op.toLowerCase()))
+ ops: loginOptions
+ .map((e) => ConfigOP(op: e['name'], icon: e['icon']))
.toList(),
curOP: curOP,
cbLogin: (Map authBody) {
@@ -506,14 +528,22 @@ Future loginDialog() async {
Text(
translate('Login'),
).marginOnly(top: MyTheme.dialogPadding),
- TextButton(
+ InkWell(
child: Icon(
Icons.close,
- size: 20,
- color: Colors.black54,
+ size: 25,
+ // No need to handle the branch of null.
+ // Because we can ensure the color is not null when debug.
+ color: Theme.of(context)
+ .textTheme
+ .titleLarge
+ ?.color
+ ?.withOpacity(0.55),
),
- onPressed: onDialogCancel,
- ).marginOnly(top: 5),
+ onTap: onDialogCancel,
+ hoverColor: Colors.red,
+ borderRadius: BorderRadius.circular(5),
+ ).marginOnly(top: 10, right: 15),
],
);
final titlePadding = EdgeInsets.fromLTRB(MyTheme.dialogPadding, 0, 0, 0);
@@ -647,9 +677,8 @@ Future verificationCodeDialog(UserPayload? user) async {
},
),
*/
- Offstage(
- offstage: !isInProgress,
- child: const LinearProgressIndicator()),
+ // NOT use Offstage to wrap LinearProgressIndicator
+ if (isInProgress) const LinearProgressIndicator(),
],
),
onCancel: close,
@@ -662,3 +691,22 @@ Future verificationCodeDialog(UserPayload? user) async {
return res;
}
+
+void logOutConfirmDialog() {
+ gFFI.dialogManager.show((setState, close, context) {
+ submit() {
+ close();
+ gFFI.userModel.logOut();
+ }
+
+ return CustomAlertDialog(
+ content: Text(translate("logout_tip")),
+ actions: [
+ dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
+ dialogButton(translate("OK"), onPressed: submit),
+ ],
+ onSubmit: submit,
+ onCancel: close,
+ );
+ });
+}
diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart
index 73210b5fe..b19034c68 100644
--- a/flutter/lib/common/widgets/overlay.dart
+++ b/flutter/lib/common/widgets/overlay.dart
@@ -26,15 +26,32 @@ class DraggableChatWindow extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Draggable(
+ return isIOS
+ ? IOSDraggable (
+ position: position,
+ chatModel: chatModel,
+ width: width,
+ height: height,
+ builder: (context) {
+ return Column(
+ children: [
+ _buildMobileAppBar(context),
+ Expanded(
+ child: ChatPage(chatModel: chatModel),
+ ),
+ ],
+ );
+ },
+ )
+ : Draggable(
checkKeyboard: true,
position: position,
width: width,
height: height,
+ chatModel: chatModel,
builder: (context, onPanUpdate) {
- return isIOS
- ? ChatPage(chatModel: chatModel)
- : Scaffold(
+ final child =
+ Scaffold(
resizeToAvoidBottomInset: false,
appBar: CustomAppBar(
onPanUpdate: onPanUpdate,
@@ -44,6 +61,10 @@ class DraggableChatWindow extends StatelessWidget {
),
body: ChatPage(chatModel: chatModel),
);
+ return Container(
+ decoration:
+ BoxDecoration(border: Border.all(color: MyTheme.border)),
+ child: child);
});
}
@@ -222,6 +243,7 @@ class Draggable extends StatefulWidget {
this.position = Offset.zero,
required this.width,
required this.height,
+ this.chatModel,
required this.builder})
: super(key: key);
@@ -230,6 +252,7 @@ class Draggable extends StatefulWidget {
final Offset position;
final double width;
final double height;
+ final ChatModel? chatModel;
final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
@override
@@ -238,6 +261,7 @@ class Draggable extends StatefulWidget {
class _DraggableState extends State {
late Offset _position;
+ late ChatModel? _chatModel;
bool _keyboardVisible = false;
double _saveHeight = 0;
double _lastBottomHeight = 0;
@@ -246,6 +270,7 @@ class _DraggableState extends State {
void initState() {
super.initState();
_position = widget.position;
+ _chatModel = widget.chatModel;
}
void onPanUpdate(DragUpdateDetails d) {
@@ -272,6 +297,7 @@ class _DraggableState extends State {
setState(() {
_position = Offset(x, y);
});
+ _chatModel?.setChatWindowPosition(_position);
}
checkScreenSize() {}
@@ -327,6 +353,107 @@ class _DraggableState extends State {
}
}
+class IOSDraggable extends StatefulWidget {
+ const IOSDraggable({
+ Key? key,
+ this.position = Offset.zero,
+ this.chatModel,
+ required this.width,
+ required this.height,
+ required this.builder})
+ : super(key: key);
+
+ final Offset position;
+ final ChatModel? chatModel;
+ final double width;
+ final double height;
+ final Widget Function(BuildContext) builder;
+
+ @override
+ _IOSDraggableState createState() => _IOSDraggableState();
+}
+
+class _IOSDraggableState extends State {
+ late Offset _position;
+ late ChatModel? _chatModel;
+ late double _width;
+ late double _height;
+ bool _keyboardVisible = false;
+ double _saveHeight = 0;
+ double _lastBottomHeight = 0;
+
+ @override
+ void initState() {
+ super.initState();
+ _position = widget.position;
+ _chatModel = widget.chatModel;
+ _width = widget.width;
+ _height = widget.height;
+ }
+
+ checkKeyboard() {
+ final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
+ final currentVisible = bottomHeight != 0;
+
+ // save
+ if (!_keyboardVisible && currentVisible) {
+ _saveHeight = _position.dy;
+ }
+
+ // reset
+ if (_lastBottomHeight > 0 && bottomHeight == 0) {
+ setState(() {
+ _position = Offset(_position.dx, _saveHeight);
+ });
+ }
+
+ // onKeyboardVisible
+ if (_keyboardVisible && currentVisible) {
+ final sumHeight = bottomHeight + _height;
+ final contextHeight = MediaQuery.of(context).size.height;
+ if (sumHeight + _position.dy > contextHeight) {
+ final y = contextHeight - sumHeight;
+ setState(() {
+ _position = Offset(_position.dx, y);
+ });
+ }
+ }
+
+ _keyboardVisible = currentVisible;
+ _lastBottomHeight = bottomHeight;
+ }
+
+@override
+ Widget build(BuildContext context) {
+ checkKeyboard();
+ return Stack(
+ children: [
+ Positioned(
+ left: _position.dx,
+ top: _position.dy,
+ child: GestureDetector(
+ onPanUpdate: (details) {
+ setState(() {
+ _position += details.delta;
+ });
+ _chatModel?.setChatWindowPosition(_position);
+ },
+ child: Material(
+ child:
+ Container(
+ width: _width,
+ height: _height,
+ decoration: BoxDecoration(border: Border.all(color: MyTheme.border)),
+ child: widget.builder(context),
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
+
class QualityMonitor extends StatelessWidget {
final QualityMonitorModel qualityMonitorModel;
QualityMonitor(this.qualityMonitorModel);
diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart
index 09d1cb521..f5af94220 100644
--- a/flutter/lib/common/widgets/peer_card.dart
+++ b/flutter/lib/common/widgets/peer_card.dart
@@ -2,9 +2,11 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:flutter_hbb/common/widgets/address_book.dart';
+import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/consts.dart';
+import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:get/get.dart';
+import 'package:provider/provider.dart';
import '../../common.dart';
import '../../common/formatter/id_formatter.dart';
@@ -12,6 +14,7 @@ import '../../models/peer_model.dart';
import '../../models/platform_model.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import '../../desktop/widgets/popup_menu.dart';
+import 'dart:math' as math;
typedef PopupMenuEntryBuilder = Future>>
Function(BuildContext);
@@ -22,11 +25,13 @@ final peerCardUiType = PeerUiType.grid.obs;
class _PeerCard extends StatefulWidget {
final Peer peer;
+ final PeerTabIndex tab;
final Function(BuildContext, String) connect;
final PopupMenuEntryBuilder popupMenuEntryBuilder;
const _PeerCard(
{required this.peer,
+ required this.tab,
required this.connect,
required this.popupMenuEntryBuilder,
Key? key})
@@ -56,65 +61,33 @@ class _PeerCardState extends State<_PeerCard>
Widget _buildMobile() {
final peer = super.widget.peer;
- final name =
- '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
-
+ final PeerTabModel peerTabModel = Provider.of(context);
return Card(
margin: EdgeInsets.symmetric(horizontal: 2),
child: GestureDetector(
- onTap: !isWebDesktop ? () => connect(context, peer.id) : null,
- onDoubleTap: isWebDesktop ? () => connect(context, peer.id) : null,
- onLongPressStart: (details) {
- final x = details.globalPosition.dx;
- final y = details.globalPosition.dy;
- _menuPos = RelativeRect.fromLTRB(x, y, x, y);
- _showPeerMenu(peer.id);
- },
- child: Container(
+ onTap: () {
+ if (peerTabModel.multiSelectionMode) {
+ peerTabModel.select(peer);
+ } else {
+ if (!isWebDesktop) {
+ connectInPeerTab(context, peer.id, widget.tab);
+ }
+ }
+ },
+ onDoubleTap: isWebDesktop
+ ? () => connectInPeerTab(context, peer.id, widget.tab)
+ : null,
+ onLongPress: () {
+ peerTabModel.select(peer);
+ },
+ child: Container(
padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
- child: Row(
- children: [
- Container(
- width: 50,
- height: 50,
- decoration: BoxDecoration(
- color: str2color('${peer.id}${peer.platform}', 0x7f),
- borderRadius: BorderRadius.circular(4),
- ),
- padding: const EdgeInsets.all(6),
- child: getPlatformImage(peer.platform)),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(children: [
- getOnline(4, peer.online),
- Text(peer.alias.isEmpty
- ? formatID(peer.id)
- : peer.alias)
- ]),
- Text(name)
- ],
- ).paddingOnly(left: 8.0),
- ),
- InkWell(
- child: const Padding(
- padding: EdgeInsets.all(12),
- child: Icon(Icons.more_vert)),
- onTapDown: (e) {
- final x = e.globalPosition.dx;
- final y = e.globalPosition.dy;
- _menuPos = RelativeRect.fromLTRB(x, y, x, y);
- },
- onTap: () {
- _showPeerMenu(peer.id);
- })
- ],
- ),
- )));
+ child: _buildPeerTile(context, peer, null)),
+ ));
}
Widget _buildDesktop() {
+ final PeerTabModel peerTabModel = Provider.of(context);
final peer = super.widget.peer;
var deco = Rx(
BoxDecoration(
@@ -144,7 +117,12 @@ class _PeerCardState extends State<_PeerCard>
);
},
child: GestureDetector(
- onDoubleTap: () => widget.connect(context, peer.id),
+ onDoubleTap:
+ peerTabModel.multiSelectionMode || peerTabModel.isShiftDown
+ ? null
+ : () => widget.connect(context, peer.id),
+ onTap: () => peerTabModel.select(peer),
+ onLongPress: () => peerTabModel.select(peer),
child: Obx(() => peerCardUiType.value == PeerUiType.grid
? _buildPeerCard(context, peer, deco)
: _buildPeerTile(context, peer, deco))),
@@ -152,74 +130,101 @@ class _PeerCardState extends State<_PeerCard>
}
Widget _buildPeerTile(
- BuildContext context, Peer peer, Rx deco) {
+ BuildContext context, Peer peer, Rx? deco) {
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
final greyStyle = TextStyle(
fontSize: 11,
color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
- final alias = bind.mainGetPeerOptionSync(id: peer.id, key: 'alias');
- return Obx(
- () => Container(
- foregroundDecoration: deco.value,
- child: Row(
- mainAxisSize: MainAxisSize.max,
- children: [
- Container(
- decoration: BoxDecoration(
- color: str2color('${peer.id}${peer.platform}', 0x7f),
- borderRadius: BorderRadius.only(
- topLeft: Radius.circular(_tileRadius),
- bottomLeft: Radius.circular(_tileRadius),
- ),
- ),
- alignment: Alignment.center,
- width: 42,
- child: getPlatformImage(peer.platform, size: 30).paddingAll(6),
- ),
- Expanded(
- child: Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.background,
- borderRadius: BorderRadius.only(
- topRight: Radius.circular(_tileRadius),
- bottomRight: Radius.circular(_tileRadius),
+ final child = Row(
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Container(
+ decoration: BoxDecoration(
+ color: str2color('${peer.id}${peer.platform}', 0x7f),
+ borderRadius: isMobile
+ ? BorderRadius.circular(_tileRadius)
+ : BorderRadius.only(
+ topLeft: Radius.circular(_tileRadius),
+ bottomLeft: Radius.circular(_tileRadius),
),
- ),
- child: Row(
- children: [
- Expanded(
- child: Column(
- children: [
- Row(children: [
- getOnline(8, peer.online),
- Expanded(
- child: Text(
- alias.isEmpty ? formatID(peer.id) : alias,
- overflow: TextOverflow.ellipsis,
- style: Theme.of(context).textTheme.titleSmall,
- )),
- ]).marginOnly(bottom: 2),
- Align(
- alignment: Alignment.centerLeft,
- child: Text(
- name,
- style: greyStyle,
- textAlign: TextAlign.start,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- ],
- ).marginOnly(top: 2),
- ),
- _actionMore(peer),
- ],
- ).paddingOnly(left: 10.0, top: 3.0),
- ),
- )
- ],
+ ),
+ alignment: Alignment.center,
+ width: isMobile ? 50 : 42,
+ height: isMobile ? 50 : null,
+ child: getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
+ .paddingAll(6),
),
- ),
+ Expanded(
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.background,
+ borderRadius: BorderRadius.only(
+ topRight: Radius.circular(_tileRadius),
+ bottomRight: Radius.circular(_tileRadius),
+ ),
+ ),
+ child: Row(
+ children: [
+ Expanded(
+ child: Column(
+ children: [
+ Row(children: [
+ getOnline(isMobile ? 4 : 8, peer.online),
+ Expanded(
+ child: Text(
+ peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
+ overflow: TextOverflow.ellipsis,
+ style: Theme.of(context).textTheme.titleSmall,
+ )),
+ ]).marginOnly(top: isMobile ? 0 : 2),
+ Align(
+ alignment: Alignment.centerLeft,
+ child: Text(
+ name,
+ style: isMobile ? null : greyStyle,
+ textAlign: TextAlign.start,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ).marginOnly(top: 2),
+ ),
+ isMobile
+ ? checkBoxOrActionMoreMobile(peer)
+ : checkBoxOrActionMoreDesktop(peer, isTile: true),
+ ],
+ ).paddingOnly(left: 10.0, top: 3.0),
+ ),
+ )
+ ],
+ );
+ final colors =
+ _frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
+ return Tooltip(
+ message: isMobile
+ ? ''
+ : peer.tags.isNotEmpty
+ ? '${translate('Tags')}: ${peer.tags.join(', ')}'
+ : '',
+ child: Stack(children: [
+ deco == null
+ ? child
+ : Obx(
+ () => Container(
+ foregroundDecoration: deco.value,
+ child: child,
+ ),
+ ),
+ if (colors.isNotEmpty)
+ Positioned(
+ top: 2,
+ right: isMobile ? 20 : 10,
+ child: CustomPaint(
+ painter: TagPainter(radius: 3, colors: colors),
+ ),
+ )
+ ]),
);
}
@@ -227,7 +232,7 @@ class _PeerCardState extends State<_PeerCard>
BuildContext context, Peer peer, Rx deco) {
final name =
'${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
- return Card(
+ final child = Card(
color: Colors.transparent,
elevation: 0,
margin: EdgeInsets.zero,
@@ -253,7 +258,7 @@ class _PeerCardState extends State<_PeerCard>
padding: const EdgeInsets.all(6),
child:
getPlatformImage(peer.platform, size: 60),
- ),
+ ).marginOnly(top: 4),
Row(
children: [
Expanded(
@@ -294,7 +299,7 @@ class _PeerCardState extends State<_PeerCard>
style: Theme.of(context).textTheme.titleSmall,
)),
]).paddingSymmetric(vertical: 8)),
- _actionMore(peer),
+ checkBoxOrActionMoreDesktop(peer, isTile: false),
],
).paddingSymmetric(horizontal: 12.0),
)
@@ -304,6 +309,87 @@ class _PeerCardState extends State<_PeerCard>
),
),
);
+
+ final colors =
+ _frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
+ return Tooltip(
+ message: peer.tags.isNotEmpty
+ ? '${translate('Tags')}: ${peer.tags.join(', ')}'
+ : '',
+ child: Stack(children: [
+ child,
+ if (colors.isNotEmpty)
+ Positioned(
+ top: 4,
+ right: 12,
+ child: CustomPaint(
+ painter: TagPainter(radius: 4, colors: colors),
+ ),
+ )
+ ]),
+ );
+ }
+
+ List _frontN(List list, int n) {
+ if (list.length <= n) {
+ return list;
+ } else {
+ return list.sublist(0, n);
+ }
+ }
+
+ Widget checkBoxOrActionMoreMobile(Peer peer) {
+ final PeerTabModel peerTabModel = Provider.of(context);
+ final selected = peerTabModel.isPeerSelected(peer.id);
+ if (peerTabModel.multiSelectionMode) {
+ return Padding(
+ padding: const EdgeInsets.all(12),
+ child: selected
+ ? Icon(
+ Icons.check_box,
+ color: MyTheme.accent,
+ )
+ : Icon(Icons.check_box_outline_blank),
+ );
+ } else {
+ return InkWell(
+ child: const Padding(
+ padding: EdgeInsets.all(12), child: Icon(Icons.more_vert)),
+ onTapDown: (e) {
+ final x = e.globalPosition.dx;
+ final y = e.globalPosition.dy;
+ _menuPos = RelativeRect.fromLTRB(x, y, x, y);
+ },
+ onTap: () {
+ _showPeerMenu(peer.id);
+ });
+ }
+ }
+
+ Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) {
+ final PeerTabModel peerTabModel = Provider.of(context);
+ final selected = peerTabModel.isPeerSelected(peer.id);
+ if (peerTabModel.multiSelectionMode) {
+ final icon = selected
+ ? Icon(
+ Icons.check_box,
+ color: MyTheme.accent,
+ )
+ : Icon(Icons.check_box_outline_blank);
+ bool last = peerTabModel.isShiftDown && peer.id == peerTabModel.lastId;
+ double right = isTile ? 4 : 0;
+ if (last) {
+ return Container(
+ decoration: BoxDecoration(
+ border: Border.all(color: MyTheme.accent, width: 1)),
+ child: icon,
+ ).marginOnly(right: right);
+ } else {
+ return icon.marginOnly(right: right);
+ }
+ } else {
+ return _actionMore(peer);
+ }
}
Widget _actionMore(Peer peer) => Listener(
@@ -332,16 +418,20 @@ class _PeerCardState extends State<_PeerCard>
abstract class BasePeerCard extends StatelessWidget {
final Peer peer;
+ final PeerTabIndex tab;
final EdgeInsets? menuPadding;
- BasePeerCard({required this.peer, this.menuPadding, Key? key})
+ BasePeerCard(
+ {required this.peer, required this.tab, this.menuPadding, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return _PeerCard(
peer: peer,
- connect: (BuildContext context, String id) => connect(context, id),
+ tab: tab,
+ connect: (BuildContext context, String id) =>
+ connectInPeerTab(context, id, tab),
popupMenuEntryBuilder: _buildPopupMenuEntry,
);
}
@@ -362,19 +452,23 @@ abstract class BasePeerCard extends StatelessWidget {
Future>> _buildMenuItems(BuildContext context);
MenuEntryBase _connectCommonAction(
- BuildContext context, String id, String title,
- {bool isFileTransfer = false,
- bool isTcpTunneling = false,
- bool isRDP = false}) {
+ BuildContext context,
+ String id,
+ String title, {
+ bool isFileTransfer = false,
+ bool isTcpTunneling = false,
+ bool isRDP = false,
+ }) {
return MenuEntryButton(
childBuilder: (TextStyle? style) => Text(
title,
style: style,
),
proc: () {
- connect(
+ connectInPeerTab(
context,
peer.id,
+ tab,
isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
@@ -388,11 +482,12 @@ abstract class BasePeerCard extends StatelessWidget {
@protected
MenuEntryBase _connectAction(BuildContext context, Peer peer) {
return _connectCommonAction(
- context,
- peer.id,
- peer.alias.isEmpty
- ? translate('Connect')
- : "${translate('Connect')} ${peer.id}");
+ context,
+ peer.id,
+ (peer.alias.isEmpty
+ ? translate('Connect')
+ : '${translate('Connect')} ${peer.id}'),
+ );
}
@protected
@@ -446,7 +541,7 @@ abstract class BasePeerCard extends StatelessWidget {
],
)),
proc: () {
- connect(context, id, isRDP: true);
+ connectInPeerTab(context, id, tab, isRDP: true);
},
padding: menuPadding,
dismissOnClicked: true,
@@ -478,21 +573,48 @@ abstract class BasePeerCard extends StatelessWidget {
),
proc: () {
bind.mainCreateShortcut(id: id);
+ showToast(translate('Successful'));
},
padding: menuPadding,
dismissOnClicked: true,
);
}
+ Future> _openNewConnInAction(
+ String id, String label, String key) async {
+ return MenuEntrySwitch(
+ switchType: SwitchType.scheckbox,
+ text: translate(label),
+ getter: () async => mainGetPeerBoolOptionSync(id, key),
+ setter: (bool v) async {
+ await bind.mainSetPeerOption(
+ id: id, key: key, value: bool2option(key, v));
+ showToast(translate('Successful'));
+ },
+ padding: menuPadding,
+ dismissOnClicked: true,
+ );
+ }
+
+ _openInTabsAction(String id) async =>
+ await _openNewConnInAction(id, 'Open in New Tab', kOptionOpenInTabs);
+
+ _openInWindowsAction(String id) async => await _openNewConnInAction(
+ id, 'Open in New Window', kOptionOpenInWindows);
+
+ _openNewConnInOptAction(String id) async =>
+ mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
+ ? await _openInWindowsAction(id)
+ : await _openInTabsAction(id);
+
@protected
Future _isForceAlwaysRelay(String id) async {
- return (await bind.mainGetPeerOption(id: id, key: 'force-always-relay'))
+ return (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay))
.isNotEmpty;
}
@protected
Future> _forceAlwaysRelayAction(String id) async {
- const option = 'force-always-relay';
return MenuEntrySwitch(
switchType: SwitchType.scheckbox,
text: translate('Always connect via relay'),
@@ -500,9 +622,11 @@ abstract class BasePeerCard extends StatelessWidget {
return await _isForceAlwaysRelay(id);
},
setter: (bool v) async {
- gFFI.abModel.setPeerForceAlwaysRelay(id, v);
await bind.mainSetPeerOption(
- id: id, key: option, value: bool2option(option, v));
+ id: id,
+ key: kOptionForceAlwaysRelay,
+ value: bool2option(kOptionForceAlwaysRelay, v));
+ showToast(translate('Successful'));
},
padding: menuPadding,
dismissOnClicked: true,
@@ -516,8 +640,23 @@ abstract class BasePeerCard extends StatelessWidget {
translate('Rename'),
style: style,
),
- proc: () {
- _rename(id);
+ proc: () async {
+ String oldName = await _getAlias(id);
+ renameDialog(
+ oldName: oldName,
+ onSubmit: (String newName) async {
+ if (newName != oldName) {
+ if (tab == PeerTabIndex.ab) {
+ gFFI.abModel.changeAlias(id: id, alias: newName);
+ await bind.mainSetPeerAlias(id: id, alias: newName);
+ gFFI.abModel.pushAb();
+ } else {
+ await bind.mainSetPeerAlias(id: id, alias: newName);
+ showToast(translate('Successful'));
+ _update();
+ }
+ }
+ });
},
padding: menuPadding,
dismissOnClicked: true,
@@ -525,9 +664,7 @@ abstract class BasePeerCard extends StatelessWidget {
}
@protected
- MenuEntryBase _removeAction(
- String id, Future Function() reloadFunc,
- {bool isLan = false}) {
+ MenuEntryBase _removeAction(String id) {
return MenuEntryButton(
childBuilder: (TextStyle? style) => Row(
children: [
@@ -546,7 +683,40 @@ abstract class BasePeerCard extends StatelessWidget {
],
),
proc: () {
- _delete(id, isLan, reloadFunc);
+ onSubmit() async {
+ switch (tab) {
+ case PeerTabIndex.recent:
+ await bind.mainRemovePeer(id: id);
+ await bind.mainLoadRecentPeers();
+ break;
+ case PeerTabIndex.fav:
+ final favs = (await bind.mainGetFav()).toList();
+ if (favs.remove(id)) {
+ await bind.mainStoreFav(favs: favs);
+ await bind.mainLoadFavPeers();
+ }
+ break;
+ case PeerTabIndex.lan:
+ await bind.mainRemoveDiscovered(id: id);
+ await bind.mainLoadLanPeers();
+ break;
+ case PeerTabIndex.ab:
+ gFFI.abModel.deletePeer(id);
+ final future = gFFI.abModel.pushAb();
+ if (await bind.mainPeerExists(id: peer.id)) {
+ gFFI.abModel.reSyncToast(future);
+ }
+ break;
+ case PeerTabIndex.group:
+ break;
+ }
+ if (tab != PeerTabIndex.ab) {
+ showToast(translate('Successful'));
+ }
+ }
+
+ deletePeerConfirmDialog(onSubmit,
+ '${translate('Delete')} "${peer.alias.isEmpty ? formatID(peer.id) : peer.alias}"?');
},
padding: menuPadding,
dismissOnClicked: true,
@@ -560,8 +730,15 @@ abstract class BasePeerCard extends StatelessWidget {
translate('Unremember Password'),
style: style,
),
- proc: () {
- bind.mainForgetPassword(id: id);
+ proc: () async {
+ bool result = gFFI.abModel.changePassword(id, '');
+ await bind.mainForgetPassword(id: id);
+ bool toast = false;
+ if (result) {
+ toast = tab == PeerTabIndex.ab;
+ gFFI.abModel.pushAb(toastIfFail: toast, toastIfSucc: toast);
+ }
+ if (!toast) showToast(translate('Successful'));
},
padding: menuPadding,
dismissOnClicked: true,
@@ -594,6 +771,7 @@ abstract class BasePeerCard extends StatelessWidget {
favs.add(id);
await bind.mainStoreFav(favs: favs);
}
+ showToast(translate('Successful'));
}();
},
padding: menuPadding,
@@ -628,6 +806,7 @@ abstract class BasePeerCard extends StatelessWidget {
await bind.mainStoreFav(favs: favs);
await reloadFunc();
}
+ showToast(translate('Successful'));
}();
},
padding: menuPadding,
@@ -644,9 +823,12 @@ abstract class BasePeerCard extends StatelessWidget {
),
proc: () {
() async {
+ if (gFFI.abModel.isFull(true)) {
+ return;
+ }
if (!gFFI.abModel.idContainBy(peer.id)) {
gFFI.abModel.addPeer(peer);
- await gFFI.abModel.pushAb();
+ gFFI.abModel.pushAb();
}
}();
},
@@ -659,123 +841,17 @@ abstract class BasePeerCard extends StatelessWidget {
Future _getAlias(String id) async =>
await bind.mainGetPeerOption(id: id, key: 'alias');
- void _rename(String id) async {
- RxBool isInProgress = false.obs;
- String name = await _getAlias(id);
- var controller = TextEditingController(text: name);
- gFFI.dialogManager.show((setState, close, context) {
- submit() async {
- isInProgress.value = true;
- String name = controller.text.trim();
- await bind.mainSetPeerAlias(id: id, alias: name);
- gFFI.abModel.setPeerAlias(id, name);
- _update();
- close();
- isInProgress.value = false;
- }
-
- return CustomAlertDialog(
- title: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(Icons.edit_rounded, color: MyTheme.accent),
- Text(translate('Rename')).paddingOnly(left: 10),
- ],
- ),
- content: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Container(
- child: Form(
- child: TextFormField(
- controller: controller,
- autofocus: true,
- decoration: InputDecoration(labelText: translate('Name')),
- ),
- ),
- ),
- Obx(() => Offstage(
- offstage: isInProgress.isFalse,
- child: const LinearProgressIndicator())),
- ],
- ),
- actions: [
- dialogButton(
- "Cancel",
- icon: Icon(Icons.close_rounded),
- onPressed: close,
- isOutline: true,
- ),
- dialogButton(
- "OK",
- icon: Icon(Icons.done_rounded),
- onPressed: submit,
- ),
- ],
- onSubmit: submit,
- onCancel: close,
- );
- });
- }
-
@protected
void _update();
-
- void _delete(String id, bool isLan, Function reloadFunc) async {
- gFFI.dialogManager.show(
- (setState, close, context) {
- submit() async {
- if (isLan) {
- await bind.mainRemoveDiscovered(id: id);
- } else {
- final favs = (await bind.mainGetFav()).toList();
- if (favs.remove(id)) {
- await bind.mainStoreFav(favs: favs);
- }
- await bind.mainRemovePeer(id: id);
- }
- await reloadFunc();
- close();
- }
-
- return CustomAlertDialog(
- title: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(
- Icons.delete_rounded,
- color: Colors.red,
- ),
- Text(translate('Delete')).paddingOnly(
- left: 10,
- ),
- ],
- ),
- content: SizedBox.shrink(),
- actions: [
- dialogButton(
- "Cancel",
- icon: Icon(Icons.close_rounded),
- onPressed: close,
- isOutline: true,
- ),
- dialogButton(
- "OK",
- icon: Icon(Icons.done_rounded),
- onPressed: submit,
- ),
- ],
- onSubmit: submit,
- onCancel: close,
- );
- },
- );
- }
}
class RecentPeerCard extends BasePeerCard {
RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
- : super(peer: peer, menuPadding: menuPadding, key: key);
+ : super(
+ peer: peer,
+ tab: PeerTabIndex.recent,
+ menuPadding: menuPadding,
+ key: key);
@override
Future>> _buildMenuItems(
@@ -790,11 +866,11 @@ class RecentPeerCard extends BasePeerCard {
if (isDesktop && peer.platform != 'Android') {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
+ // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id));
}
- menuItems.add(_wolAction(peer.id));
if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
@@ -817,9 +893,7 @@ class RecentPeerCard extends BasePeerCard {
}
menuItems.add(MenuEntryDivider());
- menuItems.add(_removeAction(peer.id, () async {
- await bind.mainLoadRecentPeers();
- }));
+ menuItems.add(_removeAction(peer.id));
return menuItems;
}
@@ -830,7 +904,11 @@ class RecentPeerCard extends BasePeerCard {
class FavoritePeerCard extends BasePeerCard {
FavoritePeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
- : super(peer: peer, menuPadding: menuPadding, key: key);
+ : super(
+ peer: peer,
+ tab: PeerTabIndex.fav,
+ menuPadding: menuPadding,
+ key: key);
@override
Future>> _buildMenuItems(
@@ -842,11 +920,11 @@ class FavoritePeerCard extends BasePeerCard {
if (isDesktop && peer.platform != 'Android') {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
+ // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id));
}
- menuItems.add(_wolAction(peer.id));
if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
@@ -866,9 +944,7 @@ class FavoritePeerCard extends BasePeerCard {
}
menuItems.add(MenuEntryDivider());
- menuItems.add(_removeAction(peer.id, () async {
- await bind.mainLoadFavPeers();
- }));
+ menuItems.add(_removeAction(peer.id));
return menuItems;
}
@@ -879,7 +955,11 @@ class FavoritePeerCard extends BasePeerCard {
class DiscoveredPeerCard extends BasePeerCard {
DiscoveredPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
- : super(peer: peer, menuPadding: menuPadding, key: key);
+ : super(
+ peer: peer,
+ tab: PeerTabIndex.lan,
+ menuPadding: menuPadding,
+ key: key);
@override
Future>> _buildMenuItems(
@@ -894,6 +974,7 @@ class DiscoveredPeerCard extends BasePeerCard {
if (isDesktop && peer.platform != 'Android') {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
+ // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id));
@@ -916,11 +997,7 @@ class DiscoveredPeerCard extends BasePeerCard {
}
menuItems.add(MenuEntryDivider());
- menuItems.add(
- _removeAction(peer.id, () async {
- await bind.mainLoadLanPeers();
- }, isLan: true),
- );
+ menuItems.add(_removeAction(peer.id));
return menuItems;
}
@@ -931,7 +1008,11 @@ class DiscoveredPeerCard extends BasePeerCard {
class AddressBookPeerCard extends BasePeerCard {
AddressBookPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
- : super(peer: peer, menuPadding: menuPadding, key: key);
+ : super(
+ peer: peer,
+ tab: PeerTabIndex.ab,
+ menuPadding: menuPadding,
+ key: key);
@override
Future>> _buildMenuItems(
@@ -943,17 +1024,17 @@ class AddressBookPeerCard extends BasePeerCard {
if (isDesktop && peer.platform != 'Android') {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
+ // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id));
}
- menuItems.add(_wolAction(peer.id));
if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
- if (await bind.mainPeerHasPassword(id: peer.id)) {
+ if (peer.hash.isNotEmpty) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
if (gFFI.abModel.tags.isNotEmpty) {
@@ -961,44 +1042,13 @@ class AddressBookPeerCard extends BasePeerCard {
}
menuItems.add(MenuEntryDivider());
- menuItems.add(_removeAction(peer.id, () async {}));
+ menuItems.add(_removeAction(peer.id));
return menuItems;
}
@protected
@override
- Future _isForceAlwaysRelay(String id) async =>
- gFFI.abModel.find(id)?.forceAlwaysRelay ?? false;
-
- @protected
- @override
- Future _getAlias(String id) async =>
- gFFI.abModel.find(id)?.alias ?? '';
-
- @protected
- @override
- void _update() => gFFI.abModel.pullAb();
-
- @protected
- @override
- MenuEntryBase _removeAction(
- String id, Future Function() reloadFunc,
- {bool isLan = false}) {
- return MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Remove'),
- style: style,
- ),
- proc: () {
- () async {
- gFFI.abModel.deletePeer(id);
- await gFFI.abModel.pushAb();
- }();
- },
- padding: super.menuPadding,
- dismissOnClicked: true,
- );
- }
+ void _update() => gFFI.abModel.pullAb(quiet: true);
@protected
MenuEntryBase _editTagAction(String id) {
@@ -1008,70 +1058,29 @@ class AddressBookPeerCard extends BasePeerCard {
style: style,
),
proc: () {
- _abEditTag(id);
+ editAbTagDialog(gFFI.abModel.getPeerTags(id), (selectedTag) async {
+ gFFI.abModel.changeTagForPeer(id, selectedTag);
+ gFFI.abModel.pushAb();
+ });
},
padding: super.menuPadding,
dismissOnClicked: true,
);
}
- void _abEditTag(String id) {
- var isInProgress = false;
-
- final tags = List.of(gFFI.abModel.tags);
- var selectedTag = gFFI.abModel.getPeerTags(id).obs;
-
- gFFI.dialogManager.show((setState, close, context) {
- submit() async {
- setState(() {
- isInProgress = true;
- });
- gFFI.abModel.changeTagForPeer(id, selectedTag);
- await gFFI.abModel.pushAb();
- close();
- }
-
- return CustomAlertDialog(
- title: Text(translate("Edit Tag")),
- content: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Container(
- padding: const EdgeInsets.symmetric(vertical: 8.0),
- child: Wrap(
- children: tags
- .map((e) => AddressBookTag(
- name: e,
- tags: selectedTag,
- onTap: () {
- if (selectedTag.contains(e)) {
- selectedTag.remove(e);
- } else {
- selectedTag.add(e);
- }
- },
- showActionMenu: false))
- .toList(growable: false),
- ),
- ),
- Offstage(
- offstage: !isInProgress, child: const LinearProgressIndicator())
- ],
- ),
- actions: [
- dialogButton("Cancel", onPressed: close, isOutline: true),
- dialogButton("OK", onPressed: submit),
- ],
- onSubmit: submit,
- onCancel: close,
- );
- });
- }
+ @protected
+ @override
+ Future _getAlias(String id) async =>
+ gFFI.abModel.find(id)?.alias ?? '';
}
class MyGroupPeerCard extends BasePeerCard {
MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
- : super(peer: peer, menuPadding: menuPadding, key: key);
+ : super(
+ peer: peer,
+ tab: PeerTabIndex.group,
+ menuPadding: menuPadding,
+ key: key);
@override
Future>> _buildMenuItems(
@@ -1083,11 +1092,11 @@ class MyGroupPeerCard extends BasePeerCard {
if (isDesktop && peer.platform != 'Android') {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
+ // menuItems.add(await _openNewConnInOptAction(peer.id));
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id));
}
- menuItems.add(_wolAction(peer.id));
if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
@@ -1123,7 +1132,7 @@ void _rdpDialog(String id) async {
id: id, key: 'rdp_username', value: username);
await bind.mainSetPeerOption(
id: id, key: 'rdp_password', value: password);
- gFFI.abModel.setRdp(id, port, username);
+ showToast(translate('Successful'));
close();
}
@@ -1251,3 +1260,64 @@ Widget build_more(BuildContext context, {bool invert = false}) {
?.color
?.withOpacity(0.5)))));
}
+
+class TagPainter extends CustomPainter {
+ final double radius;
+ late final List colors;
+
+ TagPainter({required this.radius, required List colors}) {
+ this.colors = colors.reversed.toList();
+ }
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ double x = 0;
+ double y = radius;
+ for (int i = 0; i < colors.length; i++) {
+ Paint paint = Paint();
+ paint.color = colors[i];
+ x -= radius + 1;
+ if (i == colors.length - 1) {
+ canvas.drawCircle(Offset(x, y), radius, paint);
+ } else {
+ Path path = Path();
+ path.addArc(Rect.fromCircle(center: Offset(x, y), radius: radius),
+ math.pi * 4 / 3, math.pi * 4 / 3);
+ path.addArc(
+ Rect.fromCircle(center: Offset(x - radius, y), radius: radius),
+ math.pi * 5 / 3,
+ math.pi * 2 / 3);
+ path.fillType = PathFillType.evenOdd;
+ canvas.drawPath(path, paint);
+ }
+ }
+ }
+
+ @override
+ bool shouldRepaint(covariant CustomPainter oldDelegate) {
+ return true;
+ }
+}
+
+void connectInPeerTab(BuildContext context, String id, PeerTabIndex tab,
+ {bool isFileTransfer = false,
+ bool isTcpTunneling = false,
+ bool isRDP = false}) async {
+ if (tab == PeerTabIndex.ab) {
+ // If recent peer's alias is empty, set it to ab's alias
+ // Because the platform is not set, it may not take effect, but it is more important not to display if the connection is not successful
+ Peer? p = gFFI.abModel.find(id);
+ if (p != null &&
+ p.alias.isNotEmpty &&
+ (await bind.mainGetPeerOption(id: id, key: "alias")).isEmpty) {
+ await bind.mainSetPeerAlias(
+ id: id,
+ alias: p.alias,
+ );
+ }
+ }
+ connect(context, id,
+ isFileTransfer: isFileTransfer,
+ isTcpTunneling: isTcpTunneling,
+ isRDP: isRDP);
+}
diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart
index ab85b2960..cb5413ba1 100644
--- a/flutter/lib/common/widgets/peer_tab_page.dart
+++ b/flutter/lib/common/widgets/peer_tab_page.dart
@@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart';
+import 'package:flutter_hbb/common/widgets/dialog.dart';
import 'package:flutter_hbb/common/widgets/my_group.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart';
-import 'package:flutter_hbb/common/widgets/animated_rotation_widget.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
+import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:get/get.dart';
@@ -63,7 +64,7 @@ class _PeerTabPageState extends State
@override
void initState() {
- final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type');
+ final uiType = bind.getLocalFlutterOption(k: 'peer-card-ui-type');
if (uiType != '') {
peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index
? PeerUiType.list
@@ -83,6 +84,11 @@ class _PeerTabPageState extends State
@override
Widget build(BuildContext context) {
+ final model = Provider.of(context);
+ Widget selectionWrap(Widget widget) {
+ return model.multiSelectionMode ? createMultiSelectionBar() : widget;
+ }
+
return Column(
textBaseline: TextBaseline.ideographic,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -91,43 +97,41 @@ class _PeerTabPageState extends State
height: 32,
child: Container(
padding: isDesktop ? null : EdgeInsets.symmetric(horizontal: 2),
- child: Row(
+ child: selectionWrap(Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(child: _createSwitchBar(context)),
const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
_createRefresh(),
+ _createMultiSelection(),
Offstage(
offstage: !isDesktop,
child: _createPeerViewTypeSwitch(context)),
Offstage(
offstage: gFFI.peerTabModel.currentTab == 0,
- child: PeerSortDropdown().marginOnly(left: 8),
+ child: PeerSortDropdown(),
),
Offstage(
offstage: gFFI.peerTabModel.currentTab != 3,
- child: InkWell(
- child: Obx(() => Container(
- padding: EdgeInsets.all(4.0),
- decoration: hideAbTagsPanel.value
- ? null
- : BoxDecoration(
- color: Theme.of(context).colorScheme.background,
- borderRadius: BorderRadius.circular(6)),
+ child: _hoverAction(
+ context: context,
+ hoverableWhenfalse: hideAbTagsPanel,
+ child: Tooltip(
+ message: translate('Toggle Tags'),
child: Icon(
Icons.tag_rounded,
size: 18,
- ))),
+ )),
onTap: () async {
await bind.mainSetLocalOption(
key: "hideAbTagsPanel",
value: hideAbTagsPanel.value ? "" : "Y");
hideAbTagsPanel.value = !hideAbTagsPanel.value;
},
- ).marginOnly(left: 8),
+ ),
),
],
- ),
+ )),
),
),
_createPeersView(),
@@ -167,7 +171,7 @@ class _PeerTabPageState extends State
).paddingSymmetric(horizontal: 4),
onTap: () async {
await handleTabSelection(t);
- await bind.setLocalFlutterConfig(
+ await bind.setLocalFlutterOption(
k: 'peer-tab-index', v: t.toString());
},
onHover: (value) => hover.value = value,
@@ -199,58 +203,258 @@ class _PeerTabPageState extends State
Widget _createRefresh() {
final textColor = Theme.of(context).textTheme.titleLarge?.color;
return Offstage(
- offstage: gFFI.peerTabModel.currentTab < 3, // local tab can't see effect
- child: Container(
- padding: EdgeInsets.all(4.0),
- child: AnimatedRotationWidget(
- onPressed: () {
- if (gFFI.peerTabModel.currentTab < entries.length) {
- entries[gFFI.peerTabModel.currentTab].load();
- }
- },
- child: RotatedBox(
- quarterTurns: 2,
- child: Icon(
- Icons.refresh,
- size: 18,
- color: textColor,
- ))),
- ),
+ offstage: gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index,
+ child: RefreshWidget(
+ onPressed: () {
+ if (gFFI.peerTabModel.currentTab < entries.length) {
+ entries[gFFI.peerTabModel.currentTab].load();
+ }
+ },
+ spinning: gFFI.abModel.abLoading,
+ child: RotatedBox(
+ quarterTurns: 2,
+ child: Tooltip(
+ message: translate('Refresh'),
+ child: Icon(
+ Icons.refresh,
+ size: 18,
+ color: textColor,
+ )))),
);
}
Widget _createPeerViewTypeSwitch(BuildContext context) {
final textColor = Theme.of(context).textTheme.titleLarge?.color;
final types = [PeerUiType.grid, PeerUiType.list];
- final hover = false.obs;
- final deco = BoxDecoration(
- color: Theme.of(context).colorScheme.background,
- borderRadius: BorderRadius.circular(5),
- );
- return Obx(
- () => Container(
- padding: EdgeInsets.all(4.0),
- decoration: hover.value ? deco : null,
- child: InkWell(
- onHover: (value) => hover.value = value,
- onTap: () async {
- final type = types.elementAt(
- peerCardUiType.value == types.elementAt(0) ? 1 : 0);
- await bind.setLocalFlutterConfig(
- k: 'peer-card-ui-type', v: type.index.toString());
- peerCardUiType.value = type;
- },
+ 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,
- )),
- ),
+ ))));
+ }
+
+ Widget _createMultiSelection() {
+ final textColor = Theme.of(context).textTheme.titleLarge?.color;
+ final model = Provider.of(context);
+ if (model.currentTabCachedPeers.isEmpty) return Offstage();
+ return _hoverAction(
+ context: context,
+ onTap: () {
+ model.setMultiSelectionMode(true);
+ },
+ child: Tooltip(
+ message: translate('Select'),
+ child: Icon(
+ IconFont.checkbox,
+ size: 18,
+ color: textColor,
+ )),
);
}
+
+ Widget createMultiSelectionBar() {
+ final model = Provider.of(context);
+ return Row(
+ children: [
+ deleteSelection(),
+ addSelectionToFav(),
+ addSelectionToAb(),
+ editSelectionTags(),
+ Expanded(child: Container()),
+ selectionCount(model.selectedPeers.length),
+ selectAll(),
+ closeSelection(),
+ ],
+ );
+ }
+
+ Widget deleteSelection() {
+ final model = Provider.of(context);
+ return _hoverAction(
+ context: context,
+ onTap: () {
+ onSubmit() async {
+ final peers = model.selectedPeers;
+ switch (model.currentTab) {
+ case 0:
+ peers.map((p) async {
+ await bind.mainRemovePeer(id: p.id);
+ }).toList();
+ await bind.mainLoadRecentPeers();
+ break;
+ case 1:
+ final favs = (await bind.mainGetFav()).toList();
+ peers.map((p) {
+ favs.remove(p.id);
+ }).toList();
+ await bind.mainStoreFav(favs: favs);
+ await bind.mainLoadFavPeers();
+ break;
+ case 2:
+ peers.map((p) async {
+ await bind.mainRemoveDiscovered(id: p.id);
+ }).toList();
+ await bind.mainLoadLanPeers();
+ break;
+ case 3:
+ {
+ bool hasSynced = false;
+ if (shouldSyncAb()) {
+ for (var p in peers) {
+ if (await bind.mainPeerExists(id: p.id)) {
+ hasSynced = true;
+ }
+ }
+ }
+ gFFI.abModel.deletePeers(peers.map((p) => p.id).toList());
+ final future = gFFI.abModel.pushAb();
+ if (hasSynced) {
+ gFFI.abModel.reSyncToast(future);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ gFFI.peerTabModel.setMultiSelectionMode(false);
+ if (model.currentTab != 3) showToast(translate('Successful'));
+ }
+
+ deletePeerConfirmDialog(onSubmit, translate('Delete'));
+ },
+ child: Tooltip(
+ message: translate('Delete'),
+ child: Icon(Icons.delete, color: Colors.red)));
+ }
+
+ Widget addSelectionToFav() {
+ final model = Provider.of(context);
+ return Offstage(
+ offstage:
+ model.currentTab != PeerTabIndex.recent.index, // show based on recent
+ child: _hoverAction(
+ context: context,
+ onTap: () async {
+ final peers = model.selectedPeers;
+ final favs = (await bind.mainGetFav()).toList();
+ for (var p in peers) {
+ if (!favs.contains(p.id)) {
+ favs.add(p.id);
+ }
+ }
+ await bind.mainStoreFav(favs: favs);
+ model.setMultiSelectionMode(false);
+ showToast(translate('Successful'));
+ },
+ child: Tooltip(
+ message: translate('Add to Favorites'),
+ child: Icon(model.icons[PeerTabIndex.fav.index])),
+ ).marginOnly(left: isMobile ? 11 : 6),
+ );
+ }
+
+ Widget addSelectionToAb() {
+ final model = Provider.of(context);
+ return Offstage(
+ offstage:
+ !gFFI.userModel.isLogin || model.currentTab == PeerTabIndex.ab.index,
+ child: _hoverAction(
+ context: context,
+ onTap: () {
+ if (gFFI.abModel.isFull(true)) {
+ return;
+ }
+ final peers = model.selectedPeers;
+ gFFI.abModel.addPeers(peers);
+ final future = gFFI.abModel.pushAb();
+ model.setMultiSelectionMode(false);
+ Future.delayed(Duration.zero, () async {
+ await future;
+ await Future.delayed(Duration(seconds: 2)); // toast
+ gFFI.abModel.isFull(true);
+ });
+ },
+ child: Tooltip(
+ message: translate('Add to Address Book'),
+ child: Icon(model.icons[PeerTabIndex.ab.index])),
+ ).marginOnly(left: isMobile ? 11 : 6),
+ );
+ }
+
+ Widget editSelectionTags() {
+ final model = Provider.of(context);
+ return Offstage(
+ offstage: !gFFI.userModel.isLogin ||
+ model.currentTab != PeerTabIndex.ab.index ||
+ gFFI.abModel.tags.isEmpty,
+ child: _hoverAction(
+ context: context,
+ onTap: () {
+ editAbTagDialog(List.empty(), (selectedTags) async {
+ final peers = model.selectedPeers;
+ gFFI.abModel.changeTagForPeers(
+ peers.map((p) => p.id).toList(), selectedTags);
+ gFFI.abModel.pushAb();
+ model.setMultiSelectionMode(false);
+ showToast(translate('Successful'));
+ });
+ },
+ child: Tooltip(
+ message: translate('Edit Tag'), child: Icon(Icons.tag)))
+ .marginOnly(left: isMobile ? 11 : 6),
+ );
+ }
+
+ Widget selectionCount(int count) {
+ return Align(
+ alignment: Alignment.center,
+ child: Text('$count ${translate('Selected')}'),
+ );
+ }
+
+ Widget selectAll() {
+ final model = Provider.of(context);
+ return Offstage(
+ offstage:
+ model.selectedPeers.length >= model.currentTabCachedPeers.length,
+ child: _hoverAction(
+ context: context,
+ onTap: () {
+ model.selectAll();
+ },
+ child: Tooltip(
+ message: translate('Select All'), child: Icon(Icons.select_all)),
+ ).marginOnly(left: 6),
+ );
+ }
+
+ Widget closeSelection() {
+ final model = Provider.of(context);
+ return _hoverAction(
+ context: context,
+ onTap: () {
+ model.setMultiSelectionMode(false);
+ },
+ child:
+ Tooltip(message: translate('Close'), child: Icon(Icons.clear)))
+ .marginOnly(left: 6);
+ }
}
class PeerSearchBar extends StatefulWidget {
@@ -267,18 +471,20 @@ class _PeerSearchBarState extends State {
Widget build(BuildContext context) {
return drawer
? _buildSearchBar()
- : IconButton(
- alignment: Alignment.centerRight,
+ : _hoverAction(
+ context: context,
padding: const EdgeInsets.only(right: 2),
- onPressed: () {
+ onTap: () {
setState(() {
drawer = true;
});
},
- icon: Icon(
- Icons.search_rounded,
- color: Theme.of(context).hintColor,
- ));
+ child: Tooltip(
+ message: translate('Search'),
+ child: Icon(
+ Icons.search_rounded,
+ color: Theme.of(context).hintColor,
+ )));
}
Widget _buildSearchBar() {
@@ -291,7 +497,7 @@ class _PeerSearchBarState extends State {
extentOffset: peerSearchTextController.value.text.length);
});
return Container(
- width: 120,
+ width: isMobile ? 120 : 140,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6),
@@ -337,19 +543,22 @@ class _PeerSearchBarState extends State {
),
// Icon(Icons.close),
IconButton(
- alignment: Alignment.centerRight,
- padding: const EdgeInsets.only(right: 2),
- onPressed: () {
- setState(() {
- peerSearchTextController.clear();
- peerSearchText.value = "";
- drawer = false;
- });
- },
- icon: Icon(
- Icons.close,
- color: Theme.of(context).hintColor,
- )),
+ alignment: Alignment.centerRight,
+ padding: const EdgeInsets.only(right: 2),
+ onPressed: () {
+ setState(() {
+ peerSearchTextController.clear();
+ peerSearchText.value = "";
+ drawer = false;
+ });
+ },
+ icon: Tooltip(
+ message: translate('Close'),
+ child: Icon(
+ Icons.close,
+ color: Theme.of(context).hintColor,
+ )),
+ ),
],
),
)
@@ -371,7 +580,7 @@ class _PeerSortDropdownState extends State {
void initState() {
if (!PeerSortType.values.contains(peerSort.value)) {
peerSort.value = PeerSortType.remoteId;
- bind.setLocalFlutterConfig(
+ bind.setLocalFlutterOption(
k: "peer-sorting",
v: peerSort.value,
);
@@ -401,7 +610,7 @@ class _PeerSortDropdownState extends State {
dense: true, (String? v) async {
if (v != null) {
peerSort.value = v;
- await bind.setLocalFlutterConfig(
+ await bind.setLocalFlutterOption(
k: "peer-sorting",
v: peerSort.value,
);
@@ -412,11 +621,14 @@ class _PeerSortDropdownState extends State {
}
var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
- return InkWell(
- child: Icon(
- Icons.sort_rounded,
- size: 18,
- ),
+ return _hoverAction(
+ context: context,
+ child: Tooltip(
+ message: translate('Sort by'),
+ child: Icon(
+ Icons.sort_rounded,
+ size: 18,
+ )),
onTapDown: (details) {
final x = details.globalPosition.dx;
final y = details.globalPosition.dy;
@@ -431,3 +643,90 @@ class _PeerSortDropdownState extends State {
);
}
}
+
+class RefreshWidget extends StatefulWidget {
+ final VoidCallback onPressed;
+ final Widget child;
+ final RxBool? spinning;
+ const RefreshWidget(
+ {super.key, required this.onPressed, required this.child, this.spinning});
+
+ @override
+ State createState() => RefreshWidgetState();
+}
+
+class RefreshWidgetState extends State {
+ double turns = 0.0;
+ bool hover = false;
+
+ @override
+ void initState() {
+ super.initState();
+ widget.spinning?.listen((v) {
+ if (v && mounted) {
+ setState(() {
+ turns += 1;
+ });
+ }
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final deco = BoxDecoration(
+ color: Theme.of(context).colorScheme.background,
+ borderRadius: BorderRadius.circular(6),
+ );
+ return AnimatedRotation(
+ turns: turns,
+ duration: const Duration(milliseconds: 200),
+ onEnd: () {
+ if (widget.spinning?.value == true && mounted) {
+ setState(() => turns += 1.0);
+ }
+ },
+ child: Container(
+ padding: EdgeInsets.all(4.0),
+ margin: EdgeInsets.symmetric(horizontal: 1),
+ decoration: hover ? deco : null,
+ child: InkWell(
+ onTap: () {
+ if (mounted) setState(() => turns += 1.0);
+ widget.onPressed();
+ },
+ onHover: (value) {
+ if (mounted) {
+ setState(() {
+ hover = value;
+ });
+ }
+ },
+ child: widget.child),
+ ));
+ }
+}
+
+Widget _hoverAction(
+ {required BuildContext context,
+ required Widget child,
+ required Function() onTap,
+ GestureTapDownCallback? onTapDown,
+ RxBool? hoverableWhenfalse,
+ EdgeInsetsGeometry padding = const EdgeInsets.all(4.0)}) {
+ final hover = false.obs;
+ final deco = BoxDecoration(
+ color: Theme.of(context).colorScheme.background,
+ borderRadius: BorderRadius.circular(6),
+ );
+ return Obx(
+ () => Container(
+ margin: EdgeInsets.symmetric(horizontal: 1),
+ decoration:
+ (hover.value || hoverableWhenfalse?.value == false) ? deco : null,
+ child: InkWell(
+ onHover: (value) => hover.value = value,
+ onTap: onTap,
+ onTapDown: onTapDown,
+ child: Container(padding: padding, child: child))),
+ );
+}
diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart
index 95099bcc8..0e4898fc2 100644
--- a/flutter/lib/common/widgets/peers_view.dart
+++ b/flutter/lib/common/widgets/peers_view.dart
@@ -41,7 +41,7 @@ class LoadEvent {
final peerSearchText = "".obs;
/// for peer sort, global obs value
-final peerSort = bind.getLocalFlutterConfig(k: 'peer-sorting').obs;
+final peerSort = bind.getLocalFlutterOption(k: 'peer-sorting').obs;
// list for listener
final obslist = [peerSearchText, peerSort].obs;
@@ -124,31 +124,34 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => widget.peers,
- child: Consumer(
- builder: (context, peers, child) => peers.peers.isEmpty
- ? Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(
- Icons.sentiment_very_dissatisfied_rounded,
- color: Theme.of(context).tabBarTheme.labelColor,
- size: 40,
- ).paddingOnly(bottom: 10),
- Text(
- translate(
- _emptyMessages[widget.peers.loadEvent] ?? 'Empty',
- ),
- textAlign: TextAlign.center,
- style: TextStyle(
- color: Theme.of(context).tabBarTheme.labelColor,
- ),
- ),
- ],
+ child: Consumer(builder: (context, peers, child) {
+ if (peers.peers.isEmpty) {
+ gFFI.peerTabModel.setCurrentTabCachedPeers([]);
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.sentiment_very_dissatisfied_rounded,
+ color: Theme.of(context).tabBarTheme.labelColor,
+ size: 40,
+ ).paddingOnly(bottom: 10),
+ Text(
+ translate(
+ _emptyMessages[widget.peers.loadEvent] ?? 'Empty',
+ ),
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
),
- )
- : _buildPeersView(peers),
- ),
+ ],
+ ),
+ );
+ } else {
+ return _buildPeersView(peers);
+ }
+ }),
);
}
@@ -172,6 +175,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
builder: (context, snapshot) {
if (snapshot.hasData) {
final peers = snapshot.data!;
+ gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
final cards = [];
for (final peer in peers) {
final visibilityChild = VisibilityDetector(
@@ -260,7 +264,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
// fallback to id sorting
if (!PeerSortType.values.contains(sortedBy)) {
sortedBy = PeerSortType.remoteId;
- bind.setLocalFlutterConfig(
+ bind.setLocalFlutterOption(
k: "peer-sorting",
v: sortedBy,
);
@@ -417,15 +421,12 @@ class AddressBookPeersView extends BasePeersView {
if (selectedTags.isEmpty) {
return true;
}
- if (idents.isEmpty) {
- return false;
- }
for (final tag in selectedTags) {
- if (!idents.contains(tag)) {
- return false;
+ if (idents.contains(tag)) {
+ return true;
}
}
- return true;
+ return false;
}
}
diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart
index dd39cbdfd..b00cd1fb4 100644
--- a/flutter/lib/common/widgets/remote_input.dart
+++ b/flutter/lib/common/widgets/remote_input.dart
@@ -1,7 +1,16 @@
+import 'dart:convert';
+
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:flutter/gestures.dart';
-import '../../models/input_model.dart';
+import 'package:flutter_hbb/models/platform_model.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/consts.dart';
+import 'package:flutter_hbb/models/model.dart';
+import 'package:flutter_hbb/models/input_model.dart';
+
+import './gestures.dart';
class RawKeyFocusScope extends StatelessWidget {
final FocusNode? focusNode;
@@ -30,6 +39,334 @@ class RawKeyFocusScope extends StatelessWidget {
}
}
+class RawTouchGestureDetectorRegion extends StatefulWidget {
+ final Widget child;
+ final FFI ffi;
+
+ late final InputModel inputModel = ffi.inputModel;
+ late final FfiModel ffiModel = ffi.ffiModel;
+
+ RawTouchGestureDetectorRegion({
+ required this.child,
+ required this.ffi,
+ });
+
+ @override
+ State createState() =>
+ _RawTouchGestureDetectorRegionState();
+}
+
+/// touchMode only:
+/// LongPress -> right click
+/// OneFingerPan -> start/end -> left down start/end
+/// onDoubleTapDown -> move to
+/// onLongPressDown => move to
+///
+/// mouseMode only:
+/// DoubleFiner -> right click
+/// HoldDrag -> left drag
+class _RawTouchGestureDetectorRegionState
+ extends State {
+ Offset _cacheLongPressPosition = Offset(0, 0);
+ double _mouseScrollIntegral = 0; // mouse scroll speed controller
+ double _scale = 1;
+
+ PointerDeviceKind? lastDeviceKind;
+
+ FFI get ffi => widget.ffi;
+ FfiModel get ffiModel => widget.ffiModel;
+ InputModel get inputModel => widget.inputModel;
+ bool get handleTouch => isDesktop || ffiModel.touchMode;
+ SessionID get sessionId => ffi.sessionId;
+
+ @override
+ Widget build(BuildContext context) {
+ return RawGestureDetector(
+ child: widget.child,
+ gestures: makeGestures(context),
+ );
+ }
+
+ onTapDown(TapDownDetails d) {
+ lastDeviceKind = d.kind;
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ if (handleTouch) {
+ ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+ inputModel.tapDown(MouseButtons.left);
+ }
+ }
+
+ onTapUp(TapUpDetails d) {
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ if (handleTouch) {
+ ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+ inputModel.tapUp(MouseButtons.left);
+ }
+ }
+
+ onTap() {
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ inputModel.tap(MouseButtons.left);
+ }
+
+ onDoubleTapDown(TapDownDetails d) {
+ lastDeviceKind = d.kind;
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ if (handleTouch) {
+ ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+ }
+ }
+
+ onDoubleTap() {
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ inputModel.tap(MouseButtons.left);
+ inputModel.tap(MouseButtons.left);
+ }
+
+ onLongPressDown(LongPressDownDetails d) {
+ lastDeviceKind = d.kind;
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ if (handleTouch) {
+ ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+ _cacheLongPressPosition = d.localPosition;
+ }
+ }
+
+ onLongPressUp() {
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ if (handleTouch) {
+ inputModel.tapUp(MouseButtons.left);
+ }
+ }
+
+ // for mobiles
+ onLongPress() {
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ if (handleTouch) {
+ ffi.cursorModel
+ .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
+ }
+ inputModel.tap(MouseButtons.right);
+ }
+
+ onDoubleFinerTapDown(TapDownDetails d) {
+ lastDeviceKind = d.kind;
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ // ignore for desktop and mobile
+ }
+
+ onDoubleFinerTap(TapDownDetails d) {
+ lastDeviceKind = d.kind;
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ if (isDesktop || !ffiModel.touchMode) {
+ inputModel.tap(MouseButtons.right);
+ }
+ }
+
+ onHoldDragStart(DragStartDetails d) {
+ lastDeviceKind = d.kind;
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ if (!handleTouch) {
+ inputModel.sendMouse('down', MouseButtons.left);
+ }
+ }
+
+ onHoldDragUpdate(DragUpdateDetails d) {
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ if (!handleTouch) {
+ ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
+ }
+ }
+
+ onHoldDragEnd(DragEndDetails d) {
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ if (!handleTouch) {
+ inputModel.sendMouse('up', MouseButtons.left);
+ }
+ }
+
+ onOneFingerPanStart(BuildContext context, DragStartDetails d) {
+ lastDeviceKind = d.kind ?? lastDeviceKind;
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ if (handleTouch) {
+ inputModel.sendMouse('down', MouseButtons.left);
+ ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+ } else {
+ final offset = ffi.cursorModel.offset;
+ final cursorX = offset.dx;
+ final cursorY = offset.dy;
+ final visible =
+ ffi.cursorModel.getVisibleRect().inflate(1); // extend edges
+ final size = MediaQueryData.fromView(View.of(context)).size;
+ if (!visible.contains(Offset(cursorX, cursorY))) {
+ ffi.cursorModel.move(size.width / 2, size.height / 2);
+ }
+ }
+ }
+
+ onOneFingerPanUpdate(DragUpdateDetails d) {
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
+ }
+
+ onOneFingerPanEnd(DragEndDetails d) {
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ inputModel.sendMouse('up', MouseButtons.left);
+ }
+
+ // scale + pan event
+ onTwoFingerScaleStart(ScaleStartDetails d) {
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ }
+
+ onTwoFingerScaleUpdate(ScaleUpdateDetails d) {
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ if (isDesktop) {
+ final scale = ((d.scale - _scale) * 1000).toInt();
+ _scale = d.scale;
+
+ if (scale != 0) {
+ bind.sessionSendPointer(
+ sessionId: sessionId,
+ msg: json.encode(
+ PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
+ .toJson()));
+ }
+ } else {
+ // mobile
+ ffi.canvasModel.updateScale(d.scale / _scale);
+ _scale = d.scale;
+ ffi.canvasModel.panX(d.focalPointDelta.dx);
+ ffi.canvasModel.panY(d.focalPointDelta.dy);
+ }
+ }
+
+ onTwoFingerScaleEnd(ScaleEndDetails d) {
+ if (lastDeviceKind != PointerDeviceKind.touch) {
+ return;
+ }
+ if (isDesktop) {
+ bind.sessionSendPointer(
+ sessionId: sessionId,
+ msg: json.encode(
+ PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
+ } else {
+ // mobile
+ _scale = 1;
+ bind.sessionSetViewStyle(sessionId: sessionId, value: "");
+ }
+ inputModel.sendMouse('up', MouseButtons.left);
+ }
+
+ get onHoldDragCancel => null;
+ get onThreeFingerVerticalDragUpdate => ffi.ffiModel.isPeerAndroid
+ ? null
+ : (d) {
+ _mouseScrollIntegral += d.delta.dy / 4;
+ if (_mouseScrollIntegral > 1) {
+ inputModel.scroll(1);
+ _mouseScrollIntegral = 0;
+ } else if (_mouseScrollIntegral < -1) {
+ inputModel.scroll(-1);
+ _mouseScrollIntegral = 0;
+ }
+ };
+
+ makeGestures(BuildContext context) {
+ return {
+ // Official
+ TapGestureRecognizer:
+ GestureRecognizerFactoryWithHandlers(
+ () => TapGestureRecognizer(), (instance) {
+ instance
+ ..onTapDown = onTapDown
+ ..onTapUp = onTapUp
+ ..onTap = onTap;
+ }),
+ DoubleTapGestureRecognizer:
+ GestureRecognizerFactoryWithHandlers(
+ () => DoubleTapGestureRecognizer(), (instance) {
+ instance
+ ..onDoubleTapDown = onDoubleTapDown
+ ..onDoubleTap = onDoubleTap;
+ }),
+ LongPressGestureRecognizer:
+ GestureRecognizerFactoryWithHandlers(
+ () => LongPressGestureRecognizer(), (instance) {
+ instance
+ ..onLongPressDown = onLongPressDown
+ ..onLongPressUp = onLongPressUp
+ ..onLongPress = onLongPress;
+ }),
+ // Customized
+ HoldTapMoveGestureRecognizer:
+ GestureRecognizerFactoryWithHandlers(
+ () => HoldTapMoveGestureRecognizer(),
+ (instance) => instance
+ ..onHoldDragStart = onHoldDragStart
+ ..onHoldDragUpdate = onHoldDragUpdate
+ ..onHoldDragCancel = onHoldDragCancel
+ ..onHoldDragEnd = onHoldDragEnd),
+ DoubleFinerTapGestureRecognizer:
+ GestureRecognizerFactoryWithHandlers(
+ () => DoubleFinerTapGestureRecognizer(), (instance) {
+ instance
+ ..onDoubleFinerTap = onDoubleFinerTap
+ ..onDoubleFinerTapDown = onDoubleFinerTapDown;
+ }),
+ CustomTouchGestureRecognizer:
+ GestureRecognizerFactoryWithHandlers(
+ () => CustomTouchGestureRecognizer(), (instance) {
+ instance.onOneFingerPanStart =
+ (DragStartDetails d) => onOneFingerPanStart(context, d);
+ instance
+ ..onOneFingerPanUpdate = onOneFingerPanUpdate
+ ..onOneFingerPanEnd = onOneFingerPanEnd
+ ..onTwoFingerScaleStart = onTwoFingerScaleStart
+ ..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
+ ..onTwoFingerScaleEnd = onTwoFingerScaleEnd
+ ..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate;
+ }),
+ };
+ }
+}
+
class RawPointerMouseRegion extends StatelessWidget {
final InputModel inputModel;
final Widget child;
@@ -39,36 +376,39 @@ class RawPointerMouseRegion extends StatelessWidget {
final PointerDownEventListener? onPointerDown;
final PointerUpEventListener? onPointerUp;
- RawPointerMouseRegion(
- {this.onEnter,
- this.onExit,
- this.cursor,
- this.onPointerDown,
- this.onPointerUp,
- required this.inputModel,
- required this.child});
+ RawPointerMouseRegion({
+ this.onEnter,
+ this.onExit,
+ this.cursor,
+ this.onPointerDown,
+ this.onPointerUp,
+ required this.inputModel,
+ required this.child,
+ });
@override
Widget build(BuildContext context) {
return Listener(
- onPointerHover: inputModel.onPointHoverImage,
- onPointerDown: (evt) {
- onPointerDown?.call(evt);
- inputModel.onPointDownImage(evt);
- },
- onPointerUp: (evt) {
- onPointerUp?.call(evt);
- inputModel.onPointUpImage(evt);
- },
- onPointerMove: inputModel.onPointMoveImage,
- onPointerSignal: inputModel.onPointerSignalImage,
- onPointerPanZoomStart: inputModel.onPointerPanZoomStart,
- onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate,
- onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd,
- child: MouseRegion(
- cursor: cursor ?? MouseCursor.defer,
- onEnter: onEnter,
- onExit: onExit,
- child: child));
+ onPointerHover: inputModel.onPointHoverImage,
+ onPointerDown: (evt) {
+ onPointerDown?.call(evt);
+ inputModel.onPointDownImage(evt);
+ },
+ onPointerUp: (evt) {
+ onPointerUp?.call(evt);
+ inputModel.onPointUpImage(evt);
+ },
+ onPointerMove: inputModel.onPointMoveImage,
+ onPointerSignal: inputModel.onPointerSignalImage,
+ onPointerPanZoomStart: inputModel.onPointerPanZoomStart,
+ onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate,
+ onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd,
+ child: MouseRegion(
+ cursor: cursor ?? MouseCursor.defer,
+ onEnter: onEnter,
+ onExit: onExit,
+ child: child,
+ ),
+ );
}
}
diff --git a/flutter/lib/common/widgets/setting_widgets.dart b/flutter/lib/common/widgets/setting_widgets.dart
new file mode 100644
index 000000000..771b65ab5
--- /dev/null
+++ b/flutter/lib/common/widgets/setting_widgets.dart
@@ -0,0 +1,277 @@
+import 'package:debounce_throttle/debounce_throttle.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/models/platform_model.dart';
+import 'package:get/get.dart';
+
+customImageQualityWidget(
+ {required double initQuality,
+ required double initFps,
+ required Function(double) setQuality,
+ required Function(double) setFps,
+ required bool showFps}) {
+ final qualityValue = initQuality.obs;
+ final fpsValue = initFps.obs;
+
+ final RxBool moreQualityChecked = RxBool(qualityValue.value > 100);
+ final debouncerQuality = Debouncer(
+ Duration(milliseconds: 1000),
+ onChanged: (double v) {
+ setQuality(v);
+ },
+ initialValue: qualityValue.value,
+ );
+ final debouncerFps = Debouncer(
+ Duration(milliseconds: 1000),
+ onChanged: (double v) {
+ setFps(v);
+ },
+ initialValue: fpsValue.value,
+ );
+
+ onMoreChanged(bool? value) {
+ if (value == null) return;
+ moreQualityChecked.value = value;
+ if (!value && qualityValue.value > 100) {
+ qualityValue.value = 100;
+ }
+ debouncerQuality.value = qualityValue.value;
+ }
+
+ return Column(
+ children: [
+ Obx(() => Row(
+ children: [
+ Expanded(
+ flex: 3,
+ child: Slider(
+ value: qualityValue.value,
+ min: 10.0,
+ max: moreQualityChecked.value ? 2000 : 100,
+ divisions: moreQualityChecked.value ? 199 : 18,
+ onChanged: (double value) async {
+ qualityValue.value = value;
+ debouncerQuality.value = value;
+ },
+ ),
+ ),
+ Expanded(
+ flex: 1,
+ child: Text(
+ '${qualityValue.value.round()}%',
+ style: const TextStyle(fontSize: 15),
+ )),
+ Expanded(
+ flex: isMobile ? 2 : 1,
+ child: Text(
+ translate('Bitrate'),
+ style: const TextStyle(fontSize: 15),
+ )),
+ // mobile doesn't have enough space
+ if (!isMobile)
+ Expanded(
+ flex: 1,
+ child: Row(
+ children: [
+ Checkbox(
+ value: moreQualityChecked.value,
+ onChanged: onMoreChanged,
+ ),
+ Expanded(
+ child: Text(translate('More')),
+ )
+ ],
+ ))
+ ],
+ )),
+ if (isMobile)
+ Obx(() => Row(
+ children: [
+ Expanded(
+ child: Align(
+ alignment: Alignment.centerRight,
+ child: Checkbox(
+ value: moreQualityChecked.value,
+ onChanged: onMoreChanged,
+ ),
+ ),
+ ),
+ Expanded(
+ child: Text(translate('More')),
+ )
+ ],
+ )),
+ if (showFps)
+ Obx(() => Row(
+ children: [
+ Expanded(
+ flex: 3,
+ child: Slider(
+ value: fpsValue.value,
+ min: 5.0,
+ max: 120.0,
+ divisions: 23,
+ onChanged: (double value) async {
+ fpsValue.value = value;
+ debouncerFps.value = value;
+ },
+ ),
+ ),
+ Expanded(
+ flex: 1,
+ child: Text(
+ '${fpsValue.value.round()}',
+ style: const TextStyle(fontSize: 15),
+ )),
+ Expanded(
+ flex: 2,
+ child: Text(
+ translate('FPS'),
+ style: const TextStyle(fontSize: 15),
+ ))
+ ],
+ )),
+ ],
+ );
+}
+
+customImageQualitySetting() {
+ final qualityKey = 'custom_image_quality';
+ final fpsKey = 'custom-fps';
+
+ var initQuality =
+ (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? 50.0);
+ if (initQuality < 10 || initQuality > 2000) {
+ initQuality = 50;
+ }
+ var initFps =
+ (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0);
+ if (initFps < 5 || initFps > 120) {
+ initFps = 30;
+ }
+
+ return customImageQualityWidget(
+ initQuality: initQuality,
+ initFps: initFps,
+ setQuality: (v) {
+ bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString());
+ },
+ setFps: (v) {
+ bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
+ },
+ showFps: true);
+}
+
+Future setServerConfig(
+ List controllers,
+ List errMsgs,
+ ServerConfig config,
+) async {
+ config.idServer = config.idServer.trim();
+ config.relayServer = config.relayServer.trim();
+ config.apiServer = config.apiServer.trim();
+ config.key = config.key.trim();
+ // id
+ if (config.idServer.isNotEmpty) {
+ errMsgs[0].value =
+ translate(await bind.mainTestIfValidServer(server: config.idServer));
+ if (errMsgs[0].isNotEmpty) {
+ return false;
+ }
+ }
+ // relay
+ if (config.relayServer.isNotEmpty) {
+ errMsgs[1].value =
+ translate(await bind.mainTestIfValidServer(server: config.relayServer));
+ if (errMsgs[1].isNotEmpty) {
+ return false;
+ }
+ }
+ // api
+ if (config.apiServer.isNotEmpty) {
+ if (!config.apiServer.startsWith('http://') &&
+ !config.apiServer.startsWith('https://')) {
+ errMsgs[2].value =
+ '${translate("API Server")}: ${translate("invalid_http")}';
+ return false;
+ }
+ }
+ final oldApiServer = await bind.mainGetApiServer();
+
+ // should set one by one
+ await bind.mainSetOption(
+ key: 'custom-rendezvous-server', value: config.idServer);
+ await bind.mainSetOption(key: 'relay-server', value: config.relayServer);
+ await bind.mainSetOption(key: 'api-server', value: config.apiServer);
+ await bind.mainSetOption(key: 'key', value: config.key);
+
+ final newApiServer = await bind.mainGetApiServer();
+ if (oldApiServer.isNotEmpty &&
+ oldApiServer != newApiServer &&
+ gFFI.userModel.isLogin) {
+ gFFI.userModel.logOut(apiServer: oldApiServer);
+ }
+ return true;
+}
+
+List ServerConfigImportExportWidgets(
+ List controllers,
+ List errMsgs,
+) {
+ import() {
+ Clipboard.getData(Clipboard.kTextPlain).then((value) {
+ final text = value?.text;
+ if (text != null && text.isNotEmpty) {
+ try {
+ final sc = ServerConfig.decode(text);
+ if (sc.idServer.isNotEmpty) {
+ controllers[0].text = sc.idServer;
+ controllers[1].text = sc.relayServer;
+ controllers[2].text = sc.apiServer;
+ controllers[3].text = sc.key;
+ Future success = setServerConfig(controllers, errMsgs, sc);
+ success.then((value) {
+ if (value) {
+ showToast(
+ translate('Import server configuration successfully'));
+ } else {
+ showToast(translate('Invalid server configuration'));
+ }
+ });
+ } else {
+ showToast(translate('Invalid server configuration'));
+ }
+ } catch (e) {
+ showToast(translate('Invalid server configuration'));
+ }
+ } else {
+ showToast(translate('Clipboard is empty'));
+ }
+ });
+ }
+
+ export() {
+ final text = ServerConfig(
+ idServer: controllers[0].text.trim(),
+ relayServer: controllers[1].text.trim(),
+ apiServer: controllers[2].text.trim(),
+ key: controllers[3].text.trim())
+ .encode();
+ debugPrint("ServerConfig export: $text");
+ Clipboard.setData(ClipboardData(text: text));
+ showToast(translate('Export server configuration successfully'));
+ }
+
+ return [
+ Tooltip(
+ message: translate('Import Server Config'),
+ child: IconButton(
+ icon: Icon(Icons.paste, color: Colors.grey), onPressed: import),
+ ),
+ Tooltip(
+ message: translate('Export Server Config'),
+ child: IconButton(
+ icon: Icon(Icons.copy, color: Colors.grey), onPressed: export))
+ ];
+}
diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart
index 98621a87e..4cf6fd3ea 100644
--- a/flutter/lib/common/widgets/toolbar.dart
+++ b/flutter/lib/common/widgets/toolbar.dart
@@ -11,6 +11,8 @@ import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
+bool isEditOsPassword = false;
+
class TTextMenu {
final Widget child;
final VoidCallback onPressed;
@@ -44,6 +46,28 @@ class TToggleMenu {
{required this.child, required this.value, required this.onChanged});
}
+handleOsPasswordEditIcon(
+ SessionID sessionId, OverlayDialogManager dialogManager) {
+ isEditOsPassword = true;
+ showSetOSPassword(sessionId, false, dialogManager, null, () => isEditOsPassword = false);
+}
+
+handleOsPasswordAction(
+ SessionID sessionId, OverlayDialogManager dialogManager) async {
+ if (isEditOsPassword) {
+ isEditOsPassword = false;
+ return;
+ }
+ final password =
+ await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
+ '';
+ if (password.isEmpty) {
+ showSetOSPassword(sessionId, true, dialogManager, password, () => isEditOsPassword = false);
+ } else {
+ bind.sessionInputOsPassword(sessionId: sessionId, value: password);
+ }
+}
+
List toolbarControls(BuildContext context, String id, FFI ffi) {
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
@@ -63,17 +87,26 @@ List toolbarControls(BuildContext context, String id, FFI ffi) {
// osAccount / osPassword
v.add(
TTextMenu(
- child: Row(children: [
- Text(translate(pi.is_headless ? 'OS Account' : 'OS Password')),
- Offstage(
- offstage: isDesktop,
- child:
- Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12))
- ]),
- trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)),
- onPressed: () => pi.is_headless
- ? showSetOSAccount(sessionId, ffi.dialogManager)
- : showSetOSPassword(sessionId, false, ffi.dialogManager)),
+ child: Row(children: [
+ Text(translate(pi.is_headless ? 'OS Account' : 'OS Password')),
+ Offstage(
+ offstage: isDesktop,
+ child: Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12),
+ )
+ ]),
+ trailingIcon: Transform.scale(
+ scale: 0.8,
+ child: InkWell(
+ onTap: () => pi.is_headless
+ ? showSetOSAccount(sessionId, ffi.dialogManager)
+ : handleOsPasswordEditIcon(sessionId, ffi.dialogManager),
+ child: Icon(Icons.edit),
+ ),
+ ),
+ onPressed: () => pi.is_headless
+ ? showSetOSAccount(sessionId, ffi.dialogManager)
+ : handleOsPasswordAction(sessionId, ffi.dialogManager),
+ ),
);
// paste
if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) {
diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart
index 3e664c484..7fcc7b3a7 100644
--- a/flutter/lib/consts.dart
+++ b/flutter/lib/consts.dart
@@ -5,6 +5,7 @@ import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/state_model.dart';
const double kDesktopRemoteTabBarHeight = 28.0;
+const int kInvalidWindowId = -1;
const int kMainWindowId = 0;
const String kPeerPlatformWindows = "Windows";
@@ -30,6 +31,21 @@ const String kWindowEventHide = "hide";
const String kWindowEventShow = "show";
const String kWindowConnect = "connect";
+const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
+const String kWindowEventNewFileTransfer = "new_file_transfer";
+const String kWindowEventNewPortForward = "new_port_forward";
+const String kWindowEventActiveSession = "active_session";
+const String kWindowEventGetRemoteList = "get_remote_list";
+const String kWindowEventGetSessionIdList = "get_session_id_list";
+
+const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
+const String kWindowEventGetCachedSessionData = "get_cached_session_data";
+
+const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
+const String kOptionOpenInTabs = "allow-open-in-tabs";
+const String kOptionOpenInWindows = "allow-open-in-windows";
+const String kOptionForceAlwaysRelay = "force-always-relay";
+
const String kUniLinksPrefix = "rustdesk://";
const String kUrlActionClose = "close";
@@ -39,6 +55,9 @@ const String kTabLabelSettingPage = "Settings";
const String kWindowPrefix = "wm_";
const int kWindowMainId = 0;
+const String kPointerEventKindTouch = "touch";
+const String kPointerEventKindMouse = "mouse";
+
// the executable name of the portable version
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
@@ -50,11 +69,8 @@ const int kMobileDefaultDisplayHeight = 1280;
const int kDesktopDefaultDisplayWidth = 1080;
const int kDesktopDefaultDisplayHeight = 720;
-const int kMobileMaxDisplayWidth = 720;
-const int kMobileMaxDisplayHeight = 1280;
-
-const int kDesktopMaxDisplayWidth = 1920;
-const int kDesktopMaxDisplayHeight = 1080;
+const int kMobileMaxDisplaySize = 1280;
+const int kDesktopMaxDisplaySize = 3840;
const double kDesktopFileTransferNameColWidth = 200;
const double kDesktopFileTransferModifiedColWidth = 120;
@@ -65,7 +81,7 @@ const double kDesktopFileTransferHeaderHeight = 25.0;
EdgeInsets get kDragToResizeAreaPadding =>
!kUseCompatibleUiMode && Platform.isLinux
- ? stateGlobal.fullscreen || stateGlobal.maximize
+ ? stateGlobal.fullscreen || stateGlobal.isMaximized.value
? EdgeInsets.zero
: EdgeInsets.all(5.0)
: EdgeInsets.zero;
diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart
index dc04b2f4d..6d53ecc78 100644
--- a/flutter/lib/desktop/pages/connection_page.dart
+++ b/flutter/lib/desktop/pages/connection_page.dart
@@ -9,7 +9,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:flutter_hbb/models/state_model.dart';
-import 'package:flutter_hbb/models/user_model.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:window_manager/window_manager.dart';
@@ -69,6 +68,7 @@ class _ConnectionPageState extends State
_idController.selection = TextSelection(
baseOffset: 0, extentOffset: _idController.value.text.length);
});
+ Get.put(_idController);
windowManager.addListener(this);
}
@@ -77,6 +77,9 @@ class _ConnectionPageState extends State
_idController.dispose();
_updateTimer?.cancel();
windowManager.removeListener(this);
+ if (Get.isRegistered()) {
+ Get.delete();
+ }
super.dispose();
}
@@ -103,7 +106,8 @@ class _ConnectionPageState extends State
@override
void onWindowLeaveFullScreen() {
// Restore edge border to default edge size.
- stateGlobal.resizeEdgeSize.value = kWindowEdgeSize;
+ stateGlobal.resizeEdgeSize.value =
+ stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
}
@override
diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart
index e074f7598..d458402d3 100644
--- a/flutter/lib/desktop/pages/desktop_home_page.dart
+++ b/flutter/lib/desktop/pages/desktop_home_page.dart
@@ -185,11 +185,13 @@ class _DesktopHomePageState extends State
backgroundColor: hover.value
? Theme.of(context).scaffoldBackgroundColor
: Theme.of(context).colorScheme.background,
- child: Icon(
- Icons.more_vert_outlined,
- size: 20,
- color: hover.value ? textColor : textColor?.withOpacity(0.5),
- ),
+ child: Tooltip(
+ message: translate('Settings'),
+ child: Icon(
+ Icons.more_vert_outlined,
+ size: 20,
+ color: hover.value ? textColor : textColor?.withOpacity(0.5),
+ )),
),
),
onHover: (value) => hover.value = value,
@@ -252,23 +254,28 @@ class _DesktopHomePageState extends State
onPressed: () => bind.mainUpdateTemporaryPassword(),
child: Obx(() => RotatedBox(
quarterTurns: 2,
- child: Icon(
- Icons.refresh,
- color: refreshHover.value
- ? textColor
- : Color(0xFFDDDDDD),
- size: 22,
- ))),
+ child: Tooltip(
+ message: translate('Refresh Password'),
+ child: Icon(
+ Icons.refresh,
+ color: refreshHover.value
+ ? textColor
+ : Color(0xFFDDDDDD),
+ size: 22,
+ ))
+ )),
onHover: (value) => refreshHover.value = value,
).marginOnly(right: 8, top: 4),
InkWell(
child: Obx(
- () => Icon(
- Icons.edit,
- color:
- editHover.value ? textColor : Color(0xFFDDDDDD),
- size: 22,
- ).marginOnly(right: 8, top: 4),
+ () => Tooltip(
+ message: translate('Change Password'),
+ child: Icon(
+ Icons.edit,
+ color:
+ editHover.value ? textColor : Color(0xFFDDDDDD),
+ size: 22,
+ )).marginOnly(right: 8, top: 4),
),
onTap: () => DesktopSettingPage.switch2page(1),
onHover: (value) => editHover.value = value,
@@ -319,7 +326,7 @@ class _DesktopHomePageState extends State
"Status",
"There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
"Click to download", () async {
- final Uri url = Uri.parse('https://rustdesk.com');
+ final Uri url = Uri.parse('https://rustdesk.com/download');
await launchUrl(url);
});
}
@@ -372,7 +379,7 @@ class _DesktopHomePageState extends State
} else if (Platform.isLinux) {
if (bind.mainCurrentIsWayland()) {
return buildInstallCard(
- "Warning", translate("wayland_experiment_tip"), "", () async {},
+ "Warning", "wayland_experiment_tip", "", () async {},
help: 'Help',
link: 'https://rustdesk.com/docs/en/manual/linux/#x11-required');
} else if (bind.mainIsLoginWayland()) {
@@ -527,7 +534,7 @@ class _DesktopHomePageState extends State
debugPrint(
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
if (call.method == kWindowMainWindowOnTop) {
- window_on_top(null);
+ windowOnTop(null);
} else if (call.method == kWindowGetWindowInfo) {
final screen = (await window_size.getWindowInfo()).screen;
if (screen == null) {
@@ -554,7 +561,7 @@ class _DesktopHomePageState extends State
} else if (call.method == kWindowEventShow) {
await rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
} else if (call.method == kWindowEventHide) {
- await rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]);
+ await rustDeskWinManager.unregisterActiveWindow(call.arguments['id']);
} else if (call.method == kWindowConnect) {
await connectMainDesktop(
call.arguments['id'],
@@ -563,6 +570,17 @@ class _DesktopHomePageState extends State
isRDP: call.arguments['isRDP'],
forceRelay: call.arguments['forceRelay'],
);
+ } else if (call.method == kWindowEventMoveTabToNewWindow) {
+ final args = call.arguments.split(',');
+ int? windowId;
+ try {
+ windowId = int.parse(args[0]);
+ } catch (e) {
+ debugPrint("Failed to parse window id '${call.arguments}': $e");
+ }
+ if (windowId != null) {
+ await rustDeskWinManager.moveTabToNewWindow(windowId, args[1], args[2]);
+ }
}
});
_uniLinksSubscription = listenUniLinks();
diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart
index fe617140a..3a33c7e5b 100644
--- a/flutter/lib/desktop/pages/desktop_setting_page.dart
+++ b/flutter/lib/desktop/pages/desktop_setting_page.dart
@@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
@@ -17,7 +18,6 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
-import 'package:window_manager/window_manager.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
@@ -248,7 +248,7 @@ class _General extends StatefulWidget {
class _GeneralState extends State<_General> {
final RxBool serviceStop = Get.find(tag: 'stop-service');
- RxBool serviceBtnEabled = true.obs;
+ RxBool serviceBtnEnabled = true.obs;
@override
Widget build(BuildContext context) {
@@ -300,32 +300,41 @@ class _GeneralState extends State<_General> {
return _Card(title: 'Service', children: [
Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
() async {
- serviceBtnEabled.value = false;
+ serviceBtnEnabled.value = false;
await start_service(serviceStop.value);
// enable the button after 1 second
Future.delayed(const Duration(seconds: 1), () {
- serviceBtnEabled.value = true;
+ serviceBtnEnabled.value = true;
});
}();
- }, enabled: serviceBtnEabled.value))
+ }, enabled: serviceBtnEnabled.value))
]);
}
Widget other() {
- return _Card(title: 'Other', children: [
+ final children = [
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
- 'enable-confirm-closing-tabs'),
- _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'),
- if (Platform.isLinux)
- Tooltip(
- message: translate('software_render_tip'),
- child: _OptionCheckBox(
- context,
- "Always use software rendering",
- 'allow-always-software-render',
- ),
- )
- ]);
+ 'enable-confirm-closing-tabs',
+ isServer: false),
+ _OptionCheckBox(context, 'Adaptive bitrate', 'enable-abr'),
+ _OptionCheckBox(
+ context,
+ 'Open connection in new tab',
+ kOptionOpenNewConnInTabs,
+ isServer: false,
+ ),
+ ];
+ // though this is related to GUI, but opengl problem affects all users, so put in config rather than local
+ children.add(Tooltip(
+ message: translate('software_render_tip'),
+ child: _OptionCheckBox(context, "Always use software rendering",
+ 'allow-always-software-render'),
+ ));
+ if (bind.mainShowOption(key: 'allow-linux-headless')) {
+ children.add(_OptionCheckBox(
+ context, 'Allow linux headless', 'allow-linux-headless'));
+ }
+ return _Card(title: 'Other', children: children);
}
Widget hwcodec() {
@@ -385,20 +394,17 @@ class _GeneralState extends State<_General> {
Widget record(BuildContext context) {
return futureBuilder(future: () async {
- String customDirectory =
- await bind.mainGetOption(key: 'video-save-directory');
String defaultDirectory = await bind.mainDefaultVideoSaveDirectory();
- String dir;
- if (customDirectory.isNotEmpty) {
- dir = customDirectory;
- } else {
- dir = defaultDirectory;
- }
// canLaunchUrl blocked on windows portable, user SYSTEM
- return {'dir': dir, 'canlaunch': true};
+ return {'dir': defaultDirectory, 'canlaunch': true};
}(), hasData: (data) {
Map map = data as Map;
String dir = map['dir']!;
+ String customDirectory =
+ bind.mainGetOptionSync(key: 'video-save-directory');
+ if (customDirectory.isNotEmpty) {
+ dir = customDirectory;
+ }
bool canlaunch = map['canlaunch']! as bool;
return _Card(title: 'Recording', children: [
@@ -444,8 +450,7 @@ class _GeneralState extends State<_General> {
Widget language() {
return futureBuilder(future: () async {
String langs = await bind.mainGetLangs();
- String lang = bind.mainGetLocalOption(key: kCommConfKeyLang);
- return {'langs': langs, 'lang': lang};
+ return {'langs': langs};
}(), hasData: (res) {
Map data = res as Map;
List langsList = jsonDecode(data['langs']!);
@@ -454,7 +459,7 @@ class _GeneralState extends State<_General> {
List values = langsMap.values.toList();
keys.insert(0, '');
values.insert(0, translate('Default'));
- String currentKey = data['lang']!;
+ String currentKey = bind.mainGetLocalOption(key: kCommConfKeyLang);
if (!keys.contains(currentKey)) {
currentKey = '';
}
@@ -529,10 +534,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
Widget permissions(context) {
bool enabled = !locked;
- return futureBuilder(future: () async {
- return await bind.mainGetOption(key: 'access-mode');
- }(), hasData: (data) {
- String accessMode = data! as String;
+ // Simple temp wrapper for PR check
+ tmpWrapper() {
+ String accessMode = bind.mainGetOptionSync(key: 'access-mode');
_AccessMode mode;
if (accessMode == 'full') {
mode = _AccessMode.full;
@@ -601,7 +605,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
],
),
]);
- });
+ }
+
+ return tmpWrapper();
}
Widget password(BuildContext context) {
@@ -702,8 +708,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
if (usePassword)
_SubButton('Set permanent password', setPasswordDialog,
permEnabled && !locked),
- if (usePassword)
- hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6),
+ // if (usePassword)
+ // hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6),
if (usePassword) radios[2],
]);
})));
@@ -759,17 +765,13 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
return [
_OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server',
update: update, enabled: !locked),
- futureBuilder(
- future: () async {
- String enabled = await bind.mainGetOption(key: 'direct-server');
- String port = await bind.mainGetOption(key: 'direct-access-port');
- return {'enabled': enabled, 'port': port};
- }(),
- hasData: (data) {
- bool enabled =
- option2bool('direct-server', data['enabled'].toString());
+ () {
+ // Simple temp wrapper for PR check
+ tmpWrapper() {
+ bool enabled = option2bool(
+ 'direct-server', bind.mainGetOptionSync(key: 'direct-server'));
if (!enabled) applyEnabled.value = false;
- controller.text = data['port'].toString();
+ controller.text = bind.mainGetOptionSync(key: 'direct-access-port');
return Offstage(
offstage: !enabled,
child: _SubLabeledWidget(
@@ -810,20 +812,22 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
enabled: enabled && !locked,
),
);
- },
- ),
+ }
+
+ return tmpWrapper();
+ }(),
];
}
Widget whitelist() {
bool enabled = !locked;
- return futureBuilder(future: () async {
- return await bind.mainGetOption(key: 'whitelist');
- }(), hasData: (data) {
- RxBool hasWhitelist = (data as String).isNotEmpty.obs;
+ // Simple temp wrapper for PR check
+ tmpWrapper() {
+ RxBool hasWhitelist =
+ bind.mainGetOptionSync(key: 'whitelist').isNotEmpty.obs;
update() async {
hasWhitelist.value =
- (await bind.mainGetOption(key: 'whitelist')).isNotEmpty;
+ bind.mainGetOptionSync(key: 'whitelist').isNotEmpty;
}
onChanged(bool? checked) async {
@@ -858,7 +862,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
onChanged(!hasWhitelist.value);
},
).marginOnly(left: _kCheckBoxLeftMargin);
- });
+ }
+
+ return tmpWrapper();
}
Widget hide_cm(bool enabled) {
@@ -943,11 +949,11 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
}
server(bool enabled) {
- return futureBuilder(future: () async {
- return await bind.mainGetOptions();
- }(), hasData: (data) {
+ // Simple temp wrapper for PR check
+ tmpWrapper() {
// Setting page is not modal, oldOptions should only be used when getting options, never when setting.
- Map oldOptions = jsonDecode(data! as String);
+ Map oldOptions =
+ jsonDecode(bind.mainGetOptionsSync() as String);
old(String key) {
return (oldOptions[key] ?? '').trim();
}
@@ -960,51 +966,27 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
var relayController = TextEditingController(text: old('relay-server'));
var apiController = TextEditingController(text: old('api-server'));
var keyController = TextEditingController(text: old('key'));
-
- set(String idServer, String relayServer, String apiServer,
- String key) async {
- idServer = idServer.trim();
- relayServer = relayServer.trim();
- apiServer = apiServer.trim();
- key = key.trim();
- if (idServer.isNotEmpty) {
- idErrMsg.value =
- translate(await bind.mainTestIfValidServer(server: idServer));
- if (idErrMsg.isNotEmpty) {
- return false;
- }
- }
- if (relayServer.isNotEmpty) {
- relayErrMsg.value =
- translate(await bind.mainTestIfValidServer(server: relayServer));
- if (relayErrMsg.isNotEmpty) {
- return false;
- }
- }
- if (apiServer.isNotEmpty) {
- if (!apiServer.startsWith('http://') &&
- !apiServer.startsWith('https://')) {
- apiErrMsg.value =
- '${translate("API Server")}: ${translate("invalid_http")}';
- return false;
- }
- }
- final old = await bind.mainGetOption(key: 'custom-rendezvous-server');
- if (old.isNotEmpty && old != idServer) {
- await gFFI.userModel.logOut();
- }
- // should set one by one
- await bind.mainSetOption(
- key: 'custom-rendezvous-server', value: idServer);
- await bind.mainSetOption(key: 'relay-server', value: relayServer);
- await bind.mainSetOption(key: 'api-server', value: apiServer);
- await bind.mainSetOption(key: 'key', value: key);
- return true;
- }
+ final controllers = [
+ idController,
+ relayController,
+ apiController,
+ keyController,
+ ];
+ final errMsgs = [
+ idErrMsg,
+ relayErrMsg,
+ apiErrMsg,
+ ];
submit() async {
- bool result = await set(idController.text, relayController.text,
- apiController.text, keyController.text);
+ bool result = await setServerConfig(
+ controllers,
+ errMsgs,
+ ServerConfig(
+ idServer: idController.text,
+ relayServer: relayController.text,
+ apiServer: apiController.text,
+ key: keyController.text));
if (result) {
setState(() {});
showToast(translate('Successful'));
@@ -1013,84 +995,31 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
}
}
- import() {
- Clipboard.getData(Clipboard.kTextPlain).then((value) {
- final text = value?.text;
- if (text != null && text.isNotEmpty) {
- try {
- final sc = ServerConfig.decode(text);
- if (sc.idServer.isNotEmpty) {
- idController.text = sc.idServer;
- relayController.text = sc.relayServer;
- apiController.text = sc.apiServer;
- keyController.text = sc.key;
- Future success =
- set(sc.idServer, sc.relayServer, sc.apiServer, sc.key);
- success.then((value) {
- if (value) {
- showToast(
- translate('Import server configuration successfully'));
- } else {
- showToast(translate('Invalid server configuration'));
- }
- });
- } else {
- showToast(translate('Invalid server configuration'));
- }
- } catch (e) {
- showToast(translate('Invalid server configuration'));
- }
- } else {
- showToast(translate('Clipboard is empty'));
- }
- });
- }
-
- export() {
- final text = ServerConfig(
- idServer: idController.text,
- relayServer: relayController.text,
- apiServer: apiController.text,
- key: keyController.text)
- .encode();
- debugPrint("ServerConfig export: $text");
-
- Clipboard.setData(ClipboardData(text: text));
- showToast(translate('Export server configuration successfully'));
- }
-
bool secure = !enabled;
- return _Card(title: 'ID/Relay Server', title_suffix: [
- Tooltip(
- message: translate('Import Server Config'),
- child: IconButton(
- icon: Icon(Icons.paste, color: Colors.grey),
- onPressed: enabled ? import : null),
- ),
- Tooltip(
- message: translate('Export Server Config'),
- child: IconButton(
- icon: Icon(Icons.copy, color: Colors.grey),
- onPressed: enabled ? export : null)),
- ], children: [
- Column(
+ return _Card(
+ title: 'ID/Relay Server',
+ title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs),
children: [
- Obx(() => _LabeledTextField(context, 'ID Server', idController,
- idErrMsg.value, enabled, secure)),
- Obx(() => _LabeledTextField(context, 'Relay Server',
- relayController, relayErrMsg.value, enabled, secure)),
- Obx(() => _LabeledTextField(context, 'API Server', apiController,
- apiErrMsg.value, enabled, secure)),
- _LabeledTextField(
- context, 'Key', keyController, '', enabled, secure),
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [_Button('Apply', submit, enabled: enabled)],
- ).marginOnly(top: 10),
- ],
- )
- ]);
- });
+ Column(
+ children: [
+ Obx(() => _LabeledTextField(context, 'ID Server', idController,
+ idErrMsg.value, enabled, secure)),
+ Obx(() => _LabeledTextField(context, 'Relay Server',
+ relayController, relayErrMsg.value, enabled, secure)),
+ Obx(() => _LabeledTextField(context, 'API Server',
+ apiController, apiErrMsg.value, enabled, secure)),
+ _LabeledTextField(
+ context, 'Key', keyController, '', enabled, secure),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [_Button('Apply', submit, enabled: enabled)],
+ ).marginOnly(top: 10),
+ ],
+ )
+ ]);
+ }
+
+ return tmpWrapper();
}
}
@@ -1171,15 +1100,6 @@ class _DisplayState extends State<_Display> {
}
final groupValue = bind.mainGetUserDefaultOption(key: key);
- final qualityKey = 'custom_image_quality';
- final qualityValue =
- (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
- 50.0)
- .obs;
- final fpsKey = 'custom-fps';
- final fpsValue =
- (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0)
- .obs;
return _Card(title: 'Default Image Quality', children: [
_Radio(context,
value: kRemoteImageQualityBest,
@@ -1203,64 +1123,7 @@ class _DisplayState extends State<_Display> {
onChanged: onChanged),
Offstage(
offstage: groupValue != kRemoteImageQualityCustom,
- child: Column(
- children: [
- Obx(() => Row(
- children: [
- Slider(
- value: qualityValue.value,
- min: 10.0,
- max: 100.0,
- divisions: 18,
- onChanged: (double value) async {
- qualityValue.value = value;
- await bind.mainSetUserDefaultOption(
- key: qualityKey, value: value.toString());
- },
- ),
- SizedBox(
- width: 40,
- child: Text(
- '${qualityValue.value.round()}%',
- style: const TextStyle(fontSize: 15),
- )),
- SizedBox(
- width: 50,
- child: Text(
- translate('Bitrate'),
- style: const TextStyle(fontSize: 15),
- ))
- ],
- )),
- Obx(() => Row(
- children: [
- Slider(
- value: fpsValue.value,
- min: 5.0,
- max: 120.0,
- divisions: 23,
- onChanged: (double value) async {
- fpsValue.value = value;
- await bind.mainSetUserDefaultOption(
- key: fpsKey, value: value.toString());
- },
- ),
- SizedBox(
- width: 40,
- child: Text(
- '${fpsValue.value.round()}',
- style: const TextStyle(fontSize: 15),
- )),
- SizedBox(
- width: 50,
- child: Text(
- translate('FPS'),
- style: const TextStyle(fontSize: 15),
- ))
- ],
- )),
- ],
- ),
+ child: customImageQualitySetting(),
)
]);
}
@@ -1385,7 +1248,7 @@ class _AccountState extends State<_Account> {
() => {
gFFI.userModel.userName.value.isEmpty
? loginDialog()
- : gFFI.userModel.logOut()
+ : logOutConfirmDialog()
}));
}
@@ -1503,7 +1366,7 @@ class _PluginState extends State<_Plugin> {
() => {
gFFI.userModel.userName.value.isEmpty
? loginDialog()
- : gFFI.userModel.logOut()
+ : logOutConfirmDialog()
}));
}
}
@@ -1559,7 +1422,7 @@ class _AboutState extends State<_About> {
.marginSymmetric(vertical: 4.0)),
InkWell(
onTap: () {
- launchUrlString('https://rustdesk.com/privacy');
+ launchUrlString('https://rustdesk.com/privacy.html');
},
child: Text(
translate('Privacy Statement'),
@@ -1661,54 +1524,54 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
bool reverse = false,
bool enabled = true,
Icon? checkedIcon,
- bool? fakeValue}) {
- return futureBuilder(
- future: bind.mainGetOption(key: key),
- hasData: (data) {
- bool value = option2bool(key, data.toString());
- if (reverse) value = !value;
- var ref = value.obs;
- onChanged(option) async {
- if (option != null) {
- ref.value = option;
- if (reverse) option = !option;
- String value = bool2option(key, option);
- await bind.mainSetOption(key: key, value: value);
- update?.call();
+ bool? fakeValue,
+ bool isServer = true}) {
+ bool value =
+ isServer ? mainGetBoolOptionSync(key) : mainGetLocalBoolOptionSync(key);
+ if (reverse) value = !value;
+ var ref = value.obs;
+ onChanged(option) async {
+ if (option != null) {
+ if (reverse) option = !option;
+ isServer
+ ? await mainSetBoolOption(key, option)
+ : await mainSetLocalBoolOption(key, option);
+ ref.value = isServer
+ ? mainGetBoolOptionSync(key)
+ : mainGetLocalBoolOptionSync(key);
+ update?.call();
+ }
+ }
+
+ if (fakeValue != null) {
+ ref.value = fakeValue;
+ enabled = false;
+ }
+
+ return GestureDetector(
+ child: Obx(
+ () => Row(
+ children: [
+ Checkbox(value: ref.value, onChanged: enabled ? onChanged : null)
+ .marginOnly(right: 5),
+ Offstage(
+ offstage: !ref.value || checkedIcon == null,
+ child: checkedIcon?.marginOnly(right: 5),
+ ),
+ Expanded(
+ child: Text(
+ translate(label),
+ style: TextStyle(color: _disabledTextColor(context, enabled)),
+ ))
+ ],
+ ),
+ ).marginOnly(left: _kCheckBoxLeftMargin),
+ onTap: enabled
+ ? () {
+ onChanged(!ref.value);
}
- }
-
- if (fakeValue != null) {
- ref.value = fakeValue;
- enabled = false;
- }
-
- return GestureDetector(
- child: Obx(
- () => Row(
- children: [
- Checkbox(
- value: ref.value, onChanged: enabled ? onChanged : null)
- .marginOnly(right: 5),
- Offstage(
- offstage: !ref.value || checkedIcon == null,
- child: checkedIcon?.marginOnly(right: 5),
- ),
- Expanded(
- child: Text(
- translate(label),
- style: TextStyle(color: _disabledTextColor(context, enabled)),
- ))
- ],
- ),
- ).marginOnly(left: _kCheckBoxLeftMargin),
- onTap: enabled
- ? () {
- onChanged(!ref.value);
- }
- : null,
- );
- });
+ : null,
+ );
}
// ignore: non_constant_identifier_names
@@ -1821,13 +1684,10 @@ Widget _lock(
Text(translate(label)).marginOnly(left: 5),
]).marginSymmetric(vertical: 2)),
onPressed: () async {
- bool checked = await bind.mainCheckSuperUserPermission();
+ bool checked = await callMainCheckSuperUserPermission();
if (checked) {
onUnlock();
}
- if (Platform.isMacOS) {
- await windowManager.show();
- }
},
).marginSymmetric(horizontal: 2, vertical: 4),
).marginOnly(left: _kCardLeftMargin),
@@ -2057,9 +1917,9 @@ void changeSocks5Proxy() async {
),
],
),
- Offstage(
- offstage: !isInProgress,
- child: const LinearProgressIndicator().marginOnly(top: 8))
+ // NOT use Offstage to wrap LinearProgressIndicator
+ if (isInProgress)
+ const LinearProgressIndicator().marginOnly(top: 8),
],
),
),
diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart
index eae3f1d69..d684d1535 100644
--- a/flutter/lib/desktop/pages/file_manager_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_page.dart
@@ -52,10 +52,12 @@ class FileManagerPage extends StatefulWidget {
const FileManagerPage(
{Key? key,
required this.id,
+ required this.password,
required this.tabController,
this.forceRelay})
: super(key: key);
final String id;
+ final String? password;
final bool? forceRelay;
final DesktopTabController tabController;
@@ -78,8 +80,11 @@ class _FileManagerPageState extends State
@override
void initState() {
super.initState();
- _ffi = FFI();
- _ffi.start(widget.id, isFileTransfer: true, forceRelay: widget.forceRelay);
+ _ffi = FFI(null);
+ _ffi.start(widget.id,
+ isFileTransfer: true,
+ password: widget.password,
+ forceRelay: widget.forceRelay);
WidgetsBinding.instance.addPostFrameCallback((_) {
_ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart
index d41397833..1e349c6f0 100644
--- a/flutter/lib/desktop/pages/file_manager_tab_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart
@@ -44,6 +44,7 @@ class _FileManagerTabPageState extends State {
page: FileManagerPage(
key: ValueKey(params['id']),
id: params['id'],
+ password: params['password'],
tabController: tabController,
forceRelay: params['forceRelay'],
)));
@@ -59,10 +60,10 @@ class _FileManagerTabPageState extends State {
print(
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
// for simplify, just replace connectionId
- if (call.method == "new_file_transfer") {
+ if (call.method == kWindowEventNewFileTransfer) {
final args = jsonDecode(call.arguments);
final id = args['id'];
- window_on_top(windowId());
+ windowOnTop(windowId());
tabController.add(TabInfo(
key: id,
label: id,
@@ -72,6 +73,7 @@ class _FileManagerTabPageState extends State {
page: FileManagerPage(
key: ValueKey(id),
id: id,
+ password: args['password'],
tabController: tabController,
forceRelay: args['forceRelay'],
)));
@@ -127,7 +129,7 @@ class _FileManagerTabPageState extends State {
} else {
final opt = "enable-confirm-closing-tabs";
final bool res;
- if (!option2bool(opt, await bind.mainGetOption(key: opt))) {
+ if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
res = true;
} else {
res = await closeConfirmDialog();
diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart
index 74452c7ee..44ba06263 100644
--- a/flutter/lib/desktop/pages/install_page.dart
+++ b/flutter/lib/desktop/pages/install_page.dart
@@ -160,7 +160,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
Option(desktopicon, label: 'Create desktop icon'),
Offstage(
offstage: !Platform.isWindows,
- child: Option(driverCert, label: 'idd_driver_tip'),
+ child: Option(driverCert, label: 'install_cert_tip'),
).marginOnly(top: 7),
Container(
padding: EdgeInsets.all(12),
@@ -182,10 +182,10 @@ class _InstallPageBodyState extends State<_InstallPageBody>
.marginOnly(bottom: em),
InkWell(
hoverColor: Colors.transparent,
- onTap: () =>
- launchUrlString('https://rustdesk.com/privacy'),
+ onTap: () => launchUrlString(
+ 'https://rustdesk.com/privacy.html'),
child: Tooltip(
- message: 'https://rustdesk.com/privacy',
+ message: 'https://rustdesk.com/privacy.html',
child: Row(children: [
Icon(Icons.launch_outlined, size: 16)
.marginOnly(right: 5),
@@ -204,11 +204,10 @@ class _InstallPageBodyState extends State<_InstallPageBody>
Row(
children: [
Expanded(
- child: Obx(() => Offstage(
- offstage: !showProgress.value,
- child:
- LinearProgressIndicator().marginOnly(right: 10),
- )),
+ // NOT use Offstage to wrap LinearProgressIndicator
+ child: Obx(() => showProgress.value
+ ? LinearProgressIndicator().marginOnly(right: 10)
+ : Offstage()),
),
Obx(
() => OutlinedButton.icon(
@@ -282,7 +281,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
title: null,
content: SelectionArea(
child:
- msgboxContent('info', 'Warning', 'confirm_idd_driver_tip')),
+ msgboxContent('info', 'Warning', 'confirm_install_cert_tip')),
actions: btns,
onCancel: close,
),
diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart
index 3a16ffbe1..2a173c53b 100644
--- a/flutter/lib/desktop/pages/port_forward_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_page.dart
@@ -1,5 +1,4 @@
import 'dart:convert';
-import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -8,7 +7,6 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
-import 'package:wakelock/wakelock.dart';
const double _kColumn1Width = 30;
const double _kColumn4Width = 100;
@@ -30,11 +28,13 @@ class PortForwardPage extends StatefulWidget {
const PortForwardPage(
{Key? key,
required this.id,
+ required this.password,
required this.tabController,
required this.isRDP,
this.forceRelay})
: super(key: key);
final String id;
+ final String? password;
final DesktopTabController tabController;
final bool isRDP;
final bool? forceRelay;
@@ -54,15 +54,13 @@ class _PortForwardPageState extends State
@override
void initState() {
super.initState();
- _ffi = FFI();
+ _ffi = FFI(null);
_ffi.start(widget.id,
isPortForward: true,
+ password: widget.password,
forceRelay: widget.forceRelay,
isRdp: widget.isRDP);
Get.put(_ffi, tag: 'pf_${widget.id}');
- if (!Platform.isLinux) {
- Wakelock.enable();
- }
debugPrint("Port forward page init success with id ${widget.id}");
widget.tabController.onSelected?.call(widget.id);
}
@@ -71,9 +69,6 @@ class _PortForwardPageState extends State
void dispose() {
_ffi.close();
_ffi.dialogManager.dismissAll();
- if (!Platform.isLinux) {
- Wakelock.disable();
- }
Get.delete(tag: 'pf_${widget.id}');
super.dispose();
}
diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart
index 751fc696c..621f393e0 100644
--- a/flutter/lib/desktop/pages/port_forward_tab_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart
@@ -43,6 +43,7 @@ class _PortForwardTabPageState extends State {
page: PortForwardPage(
key: ValueKey(params['id']),
id: params['id'],
+ password: params['password'],
tabController: tabController,
isRDP: isRDP,
forceRelay: params['forceRelay'],
@@ -59,11 +60,11 @@ class _PortForwardTabPageState extends State {
debugPrint(
"[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId");
// for simplify, just replace connectionId
- if (call.method == "new_port_forward") {
+ if (call.method == kWindowEventNewPortForward) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final isRDP = args['isRDP'];
- window_on_top(windowId());
+ windowOnTop(windowId());
if (tabController.state.value.tabs.indexWhere((e) => e.key == id) >=
0) {
debugPrint("port forward $id exists");
@@ -77,6 +78,7 @@ class _PortForwardTabPageState extends State {
page: PortForwardPage(
key: ValueKey(args['id']),
id: id,
+ password: args['password'],
isRDP: isRDP,
tabController: tabController,
forceRelay: args['forceRelay'],
diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart
index 849971a41..f265f1895 100644
--- a/flutter/lib/desktop/pages/remote_page.dart
+++ b/flutter/lib/desktop/pages/remote_page.dart
@@ -18,6 +18,7 @@ import '../../common/widgets/remote_input.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../models/model.dart';
+import '../../models/desktop_render_texture.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import '../../utils/image.dart';
@@ -27,10 +28,14 @@ import '../widgets/tabbar_widget.dart';
final SimpleWrapper _firstEnterImage = SimpleWrapper(false);
+final Map closeSessionOnDispose = {};
+
class RemotePage extends StatefulWidget {
RemotePage({
Key? key,
required this.id,
+ required this.sessionId,
+ required this.tabWindowId,
required this.password,
required this.toolbarState,
required this.tabController,
@@ -39,6 +44,8 @@ class RemotePage extends StatefulWidget {
}) : super(key: key);
final String id;
+ final SessionID? sessionId;
+ final int? tabWindowId;
final String? password;
final ToolbarState toolbarState;
final String? switchUuid;
@@ -66,9 +73,7 @@ class _RemotePageState extends State
late RxBool _zoomCursor;
late RxBool _remoteCursorMoved;
late RxBool _keyboardEnabled;
- late RxInt _textureId;
- late int _textureKey;
- final useTextureRender = bind.mainUseTextureRender();
+ late RenderTexture _renderTexture;
final _blockableOverlayState = BlockableOverlayState();
@@ -86,15 +91,13 @@ class _RemotePageState extends State
_showRemoteCursor = ShowRemoteCursorState.find(id);
_keyboardEnabled = KeyboardEnabledState.find(id);
_remoteCursorMoved = RemoteCursorMovedState.find(id);
- _textureKey = newTextureId;
- _textureId = RxInt(-1);
}
@override
void initState() {
super.initState();
_initStates(widget.id);
- _ffi = FFI();
+ _ffi = FFI(widget.sessionId);
Get.put(_ffi, tag: widget.id);
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded(
@@ -105,6 +108,7 @@ class _RemotePageState extends State
password: widget.password,
switchUuid: widget.switchUuid,
forceRelay: widget.forceRelay,
+ tabWindowId: widget.tabWindowId,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
@@ -115,17 +119,9 @@ class _RemotePageState extends State
Wakelock.enable();
}
// Register texture.
- _textureId.value = -1;
- if (useTextureRender) {
- textureRenderer.createTexture(_textureKey).then((id) async {
- debugPrint("id: $id, texture_key: $_textureKey");
- if (id != -1) {
- final ptr = await textureRenderer.getTexturePtr(_textureKey);
- platformFFI.registerTexture(sessionId, ptr);
- _textureId.value = id;
- }
- });
- }
+ _renderTexture = RenderTexture();
+ _renderTexture.create(sessionId);
+
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
@@ -206,26 +202,25 @@ class _RemotePageState extends State
@override
Future dispose() async {
+ final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
+
// https://github.com/flutter/flutter/issues/64935
super.dispose();
- debugPrint("REMOTE PAGE dispose ${widget.id}");
- if (useTextureRender) {
- platformFFI.registerTexture(sessionId, 0);
- // sleep for a while to avoid the texture is used after it's unregistered.
- await Future.delayed(Duration(milliseconds: 100));
- await textureRenderer.closeTexture(_textureKey);
- }
+ debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
+ await _renderTexture.destroy(closeSession);
// ensure we leave this session, this is a double check
bind.sessionEnterOrLeave(sessionId: sessionId, enter: false);
DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay();
_ffi.recordingModel.onClose();
_rawKeyFocusNode.dispose();
- await _ffi.close();
+ await _ffi.close(closeSession: closeSession);
_timer?.cancel();
_ffi.dialogManager.dismissAll();
- await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
- overlays: SystemUiOverlay.values);
+ if (closeSession) {
+ await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
+ overlays: SystemUiOverlay.values);
+ }
if (!Platform.isLinux) {
await Wakelock.disable();
}
@@ -233,49 +228,70 @@ class _RemotePageState extends State
removeSharedStates(widget.id);
}
- Widget buildBody(BuildContext context) {
- return Scaffold(
- backgroundColor: Theme.of(context).colorScheme.background,
-
- /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
- /// see override build() in [BlockableOverlay]
- body: BlockableOverlay(
+ Widget emptyOverlay() => BlockableOverlay(
+ /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
+ /// see override build() in [BlockableOverlay]
state: _blockableOverlayState,
underlying: Container(
- color: Colors.black,
- child: RawKeyFocusScope(
- focusNode: _rawKeyFocusNode,
- onFocusChange: (bool imageFocused) {
- debugPrint(
- "onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
- // See [onWindowBlur].
- if (Platform.isWindows) {
- if (_isWindowBlur) {
- imageFocused = false;
- Future.delayed(Duration.zero, () {
- _rawKeyFocusNode.unfocus();
- });
+ color: Colors.transparent,
+ ),
+ );
+
+ Widget buildBody(BuildContext context) {
+ remoteToolbar(BuildContext context) => RemoteToolbar(
+ id: widget.id,
+ ffi: _ffi,
+ state: widget.toolbarState,
+ onEnterOrLeaveImageSetter: (func) =>
+ _onEnterOrLeaveImage4Toolbar = func,
+ onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
+ );
+ return Scaffold(
+ backgroundColor: Theme.of(context).colorScheme.background,
+ body: Stack(
+ children: [
+ Container(
+ color: Colors.black,
+ child: RawKeyFocusScope(
+ focusNode: _rawKeyFocusNode,
+ onFocusChange: (bool imageFocused) {
+ debugPrint(
+ "onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
+ // See [onWindowBlur].
+ if (Platform.isWindows) {
+ if (_isWindowBlur) {
+ imageFocused = false;
+ Future.delayed(Duration.zero, () {
+ _rawKeyFocusNode.unfocus();
+ });
+ }
+ if (imageFocused) {
+ _ffi.inputModel.enterOrLeave(true);
+ } else {
+ _ffi.inputModel.enterOrLeave(false);
+ }
}
- if (imageFocused) {
- _ffi.inputModel.enterOrLeave(true);
- } else {
- _ffi.inputModel.enterOrLeave(false);
- }
- }
- },
- inputModel: _ffi.inputModel,
- child: getBodyForDesktop(context))),
- upperLayer: [
- OverlayEntry(
- builder: (context) => RemoteToolbar(
- id: widget.id,
- ffi: _ffi,
- state: widget.toolbarState,
- onEnterOrLeaveImageSetter: (func) =>
- _onEnterOrLeaveImage4Toolbar = func,
- onEnterOrLeaveImageCleaner: () =>
- _onEnterOrLeaveImage4Toolbar = null,
- ))
+ },
+ inputModel: _ffi.inputModel,
+ child: getBodyForDesktop(context))),
+ Obx(() => Stack(
+ children: [
+ _ffi.ffiModel.pi.isSet.isTrue &&
+ _ffi.ffiModel.waitForFirstImage.isTrue
+ ? emptyOverlay()
+ : () {
+ _ffi.ffiModel.tryShowAndroidActionsOverlay();
+ return Offstage();
+ }(),
+ // Use Overlay to enable rebuild every time on menu button click.
+ _ffi.ffiModel.pi.isSet.isTrue
+ ? Overlay(initialEntries: [
+ OverlayEntry(builder: remoteToolbar)
+ ])
+ : remoteToolbar(context),
+ _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
+ ],
+ )),
],
),
);
@@ -337,6 +353,17 @@ class _RemotePageState extends State
}
}
+ Widget _buildRawTouchAndPointerRegion(
+ Widget child,
+ PointerEnterEventListener? onEnter,
+ PointerExitEventListener? onExit,
+ ) {
+ return RawTouchGestureDetectorRegion(
+ child: _buildRawPointerMouseRegion(child, onEnter, onExit),
+ ffi: _ffi,
+ );
+ }
+
Widget _buildRawPointerMouseRegion(
Widget child,
PointerEnterEventListener? onEnter,
@@ -381,10 +408,10 @@ class _RemotePageState extends State
cursorOverImage: _cursorOverImage,
keyboardEnabled: _keyboardEnabled,
remoteCursorMoved: _remoteCursorMoved,
- textureId: _textureId,
- useTextureRender: useTextureRender,
+ textureId: _renderTexture.textureId,
+ useTextureRender: _renderTexture.useTextureRender,
listenerBuilder: (child) =>
- _buildRawPointerMouseRegion(child, enterView, leaveView),
+ _buildRawTouchAndPointerRegion(child, enterView, leaveView),
);
}))
];
@@ -401,7 +428,7 @@ class _RemotePageState extends State
Positioned(
top: 10,
right: 10,
- child: _buildRawPointerMouseRegion(
+ child: _buildRawTouchAndPointerRegion(
QualityMonitor(_ffi.qualityMonitorModel), null, null),
),
);
diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart
index 2333e343c..063fe49d8 100644
--- a/flutter/lib/desktop/pages/remote_tab_page.dart
+++ b/flutter/lib/desktop/pages/remote_tab_page.dart
@@ -1,10 +1,10 @@
import 'dart:convert';
+import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart';
@@ -20,6 +20,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:bot_toast/bot_toast.dart';
+import '../../common/widgets/dialog.dart';
import '../../models/platform_model.dart';
class _MenuTheme {
@@ -46,35 +47,39 @@ class _ConnectionTabPageState extends State {
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
late ToolbarState _toolbarState;
+ String? peerId;
var connectionMap = RxList.empty(growable: true);
_ConnectionTabPageState(Map params) {
_toolbarState = ToolbarState();
RemoteCountState.init();
- final peerId = params['id'];
+ peerId = params['id'];
+ final sessionId = params['session_id'];
+ final tabWindowId = params['tab_window_id'];
if (peerId != null) {
- ConnectionTypeState.init(peerId);
+ ConnectionTypeState.init(peerId!);
tabController.onSelected = (id) {
- final remotePage = tabController.state.value.tabs
- .firstWhereOrNull((tab) => tab.key == id)
- ?.page;
+ final remotePage = tabController.widget(id);
if (remotePage is RemotePage) {
final ffi = remotePage.ffi;
bind.setCurSessionId(sessionId: ffi.sessionId);
}
WindowController.fromWindowId(windowId())
.setTitle(getWindowNameWithId(id));
+ UnreadChatCountState.find(id).value = 0;
};
tabController.add(TabInfo(
- key: peerId,
- label: peerId,
+ key: peerId!,
+ label: peerId!,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(peerId),
page: RemotePage(
key: ValueKey(peerId),
- id: peerId,
+ id: peerId!,
+ sessionId: sessionId == null ? null : SessionID(sessionId),
+ tabWindowId: tabWindowId,
password: params['password'],
toolbarState: _toolbarState,
tabController: tabController,
@@ -96,12 +101,20 @@ class _ConnectionTabPageState extends State {
print(
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
+ dynamic returnValue;
// for simplify, just replace connectionId
- if (call.method == "new_remote_desktop") {
+ if (call.method == kWindowEventNewRemoteDesktop) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final switchUuid = args['switch_uuid'];
- window_on_top(windowId());
+ final sessionId = args['session_id'];
+ final tabWindowId = args['tab_window_id'];
+ windowOnTop(windowId());
+ if (tabController.length == 0) {
+ if (Platform.isMacOS && stateGlobal.closeOnFullscreen) {
+ stateGlobal.setFullscreen(true);
+ }
+ }
ConnectionTypeState.init(id);
_toolbarState.setShow(
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
@@ -114,6 +127,8 @@ class _ConnectionTabPageState extends State {
page: RemotePage(
key: ValueKey(id),
id: id,
+ sessionId: sessionId == null ? null : SessionID(sessionId),
+ tabWindowId: tabWindowId,
password: args['password'],
toolbarState: _toolbarState,
tabController: tabController,
@@ -127,11 +142,49 @@ class _ConnectionTabPageState extends State {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
+ } else if (call.method == kWindowEventActiveSession) {
+ final jumpOk = tabController.jumpToByKey(call.arguments);
+ if (jumpOk) {
+ windowOnTop(windowId());
+ }
+ return jumpOk;
+ } else if (call.method == kWindowEventGetRemoteList) {
+ return tabController.state.value.tabs
+ .map((e) => e.key)
+ .toList()
+ .join(',');
+ } else if (call.method == kWindowEventGetSessionIdList) {
+ return tabController.state.value.tabs
+ .map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
+ .toList()
+ .join(';');
+ } else if (call.method == kWindowEventGetCachedSessionData) {
+ // Ready to show new window and close old tab.
+ final peerId = call.arguments;
+ try {
+ final remotePage = tabController.state.value.tabs
+ .firstWhere((tab) => tab.key == peerId)
+ .page as RemotePage;
+ returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
+ } catch (e) {
+ debugPrint('Failed to get cached session data: $e');
+ }
+ if (returnValue != null) {
+ closeSessionOnDispose[peerId] = false;
+ tabController.closeBy(peerId);
+ }
}
_update_remote_count();
+ return returnValue;
});
Future.delayed(Duration.zero, () {
- restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId());
+ restoreWindowPosition(
+ WindowType.RemoteDesktop,
+ windowId: windowId(),
+ peerId: tabController.state.value.tabs.isEmpty
+ ? null
+ : tabController.state.value.tabs[0].key,
+ );
});
}
@@ -173,7 +226,7 @@ class _ConnectionTabPageState extends State {
connectionType.secure.value == ConnectionType.strSecure;
bool direct =
connectionType.direct.value == ConnectionType.strDirect;
- var msgConn;
+ String msgConn;
if (secure && direct) {
msgConn = translate("Direct and encrypted connection");
} else if (secure && !direct) {
@@ -185,6 +238,9 @@ class _ConnectionTabPageState extends State {
}
var msgFingerprint = '${translate('Fingerprint')}:\n';
var fingerprint = FingerprintState.find(key).value;
+ if (fingerprint.isEmpty) {
+ fingerprint = 'N/A';
+ }
if (fingerprint.length > 5 * 8) {
var first = fingerprint.substring(0, 39);
var second = fingerprint.substring(40);
@@ -206,6 +262,8 @@ class _ConnectionTabPageState extends State {
).paddingOnly(right: 5),
),
label,
+ unreadMessageCountBuilder(UnreadChatCountState.find(key))
+ .marginOnly(left: 4),
],
);
@@ -214,7 +272,11 @@ class _ConnectionTabPageState extends State {
if (e.kind != ui.PointerDeviceKind.mouse) {
return;
}
- if (e.buttons == 2) {
+ final remotePage = tabController.state.value.tabs
+ .firstWhere((tab) => tab.key == key)
+ .page as RemotePage;
+ if (remotePage.ffi.ffiModel.pi.isSet.isTrue &&
+ e.buttons == 2) {
showRightMenu(
(CancelFunc cancelFunc) {
return _tabMenuBuilder(key, cancelFunc);
@@ -255,17 +317,6 @@ class _ConnectionTabPageState extends State {
final perms = ffi.ffiModel.permissions;
final sessionId = ffi.sessionId;
menu.addAll([
- MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Close'),
- style: style,
- ),
- proc: () {
- tabController.closeBy(key);
- cancelFunc();
- },
- padding: padding,
- ),
MenuEntryButton(
childBuilder: (TextStyle? style) => Obx(() => Text(
translate(
@@ -278,33 +329,42 @@ class _ConnectionTabPageState extends State {
},
padding: padding,
),
- MenuEntryDivider(),
- RemoteMenuEntry.viewStyle(
- key,
- ffi,
- padding,
- dismissFunc: cancelFunc,
- ),
]);
- if (!ffi.canvasModel.cursorEmbedded &&
- !ffi.ffiModel.viewOnly &&
- !pi.is_wayland) {
- menu.add(MenuEntryDivider());
- menu.add(RemoteMenuEntry.showRemoteCursor(
- key,
- sessionId,
- padding,
- dismissFunc: cancelFunc,
+ if (tabController.state.value.tabs.length > 1) {
+ final splitAction = MenuEntryButton(
+ childBuilder: (TextStyle? style) => Text(
+ translate('Move tab to new window'),
+ style: style,
+ ),
+ proc: () async {
+ await DesktopMultiWindow.invokeMethod(kMainWindowId,
+ kWindowEventMoveTabToNewWindow, '${windowId()},$key,$sessionId');
+ cancelFunc();
+ },
+ padding: padding,
+ );
+ menu.insert(1, splitAction);
+ }
+
+ if (perms['restart'] != false &&
+ (pi.platform == kPeerPlatformLinux ||
+ pi.platform == kPeerPlatformWindows ||
+ pi.platform == kPeerPlatformMacOS)) {
+ menu.add(MenuEntryButton(
+ childBuilder: (TextStyle? style) => Text(
+ translate('Restart Remote Device'),
+ style: style,
+ ),
+ proc: () => showRestartRemoteDevice(
+ pi, peerId ?? '', sessionId, ffi.dialogManager),
+ padding: padding,
+ dismissOnClicked: true,
+ dismissCallback: cancelFunc,
));
}
if (perms['keyboard'] != false && !ffi.ffiModel.viewOnly) {
- if (perms['clipboard'] != false) {
- menu.add(RemoteMenuEntry.disableClipboard(sessionId, padding,
- dismissFunc: cancelFunc));
- }
-
menu.add(RemoteMenuEntry.insertLock(sessionId, padding,
dismissFunc: cancelFunc));
@@ -314,16 +374,30 @@ class _ConnectionTabPageState extends State {
}
}
- menu.add(MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Copy Fingerprint'),
- style: style,
+ menu.addAll([
+ MenuEntryDivider(),
+ MenuEntryButton(
+ childBuilder: (TextStyle? style) => Text(
+ translate('Copy Fingerprint'),
+ style: style,
+ ),
+ proc: () => onCopyFingerprint(FingerprintState.find(key).value),
+ padding: padding,
+ dismissOnClicked: true,
+ dismissCallback: cancelFunc,
),
- proc: () => onCopyFingerprint(FingerprintState.find(key).value),
- padding: padding,
- dismissOnClicked: true,
- dismissCallback: cancelFunc,
- ));
+ MenuEntryButton(
+ childBuilder: (TextStyle? style) => Text(
+ translate('Close'),
+ style: style,
+ ),
+ proc: () {
+ tabController.closeBy(key);
+ cancelFunc();
+ },
+ padding: padding,
+ )
+ ]);
return mod_menu.PopupMenu(
items: menu
@@ -342,6 +416,7 @@ class _ConnectionTabPageState extends State {
void onRemoveId(String id) async {
if (tabController.state.value.tabs.isEmpty) {
await WindowController.fromWindowId(windowId()).close();
+ stateGlobal.setFullscreen(false, procWnd: false);
}
ConnectionTypeState.delete(id);
_update_remote_count();
@@ -359,7 +434,7 @@ class _ConnectionTabPageState extends State {
} else {
final opt = "enable-confirm-closing-tabs";
final bool res;
- if (!option2bool(opt, await bind.mainGetOption(key: opt))) {
+ if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
res = true;
} else {
res = await closeConfirmDialog();
diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart
index 3ea735d25..2ef04b244 100644
--- a/flutter/lib/desktop/pages/server_page.dart
+++ b/flutter/lib/desktop/pages/server_page.dart
@@ -2,6 +2,7 @@
import 'dart:async';
import 'dart:io';
+import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
@@ -9,12 +10,14 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:get/get.dart';
+import 'package:percent_indicator/linear_percent_indicator.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../common.dart';
import '../../common/widgets/chat_page.dart';
+import '../../models/file_model.dart';
import '../../models/platform_model.dart';
import '../../models/server_model.dart';
@@ -32,6 +35,7 @@ class _DesktopServerPageState extends State
void initState() {
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
windowManager.addListener(this);
+ Get.put(tabController);
tabController.onRemoved = (_, id) {
onRemoveId(id);
};
@@ -100,11 +104,18 @@ class ConnectionManagerState extends State