From 45c66060e5f6a0fe812ad2fb13b1b4889df82b3d Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Wed, 8 Feb 2023 22:20:48 +0530 Subject: [PATCH 001/202] devcontainer configuration --- .devcontainer/Dockerfile | 19 +++++++++++++++++++ .devcontainer/devcontainer.json | 29 +++++++++++++++++++++++++++++ .gitignore | 5 +++++ 3 files changed, 53 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..0381ff966 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,19 @@ +FROM debian + +WORKDIR / +RUN apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev + +RUN git clone https://github.com/microsoft/vcpkg && cd vcpkg && git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82 +RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics +RUN /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus + +RUN groupadd -r user && useradd -r -g user user --home /home/user && mkdir -p /home/user && chown user /home/user && echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user +WORKDIR /home/user +RUN wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +USER user +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh +RUN chmod +x rustup.sh +RUN ./rustup.sh -y + +USER root +ENV HOME=/home/user diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..24ba9a915 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,29 @@ +{ + "name": "rustdesk", + "build": { + "dockerfile": "Dockerfile", + "args": { + "BUILDKIT_INLINE_CACHE": "0" + } + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/home/user/rustdesk,type=bind,consistency=cache", + "workspaceFolder": "/home/user/rustdesk", + "postStartCommand": "./entrypoint", + "remoteUser": "user", + "customizations": { + "vscode": { + "extensions": [ + "vadimcn.vscode-lldb", + "mutantdino.resourcemonitor", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "serayuzgur.crates" + ], + "settings": { + "files.watcherExclude": { + "**/target/**": true + } + } + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index fd5b5955e..a71c71a4e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,8 @@ flatpak/.flatpak-builder/debian-binary flatpak/build/** # bridge file lib/generated_bridge.dart +# vscode devcontainer +.gitconfig +.vscode-server/ +.ssh +.devcontainer/.* From 244cfa25f14f9ee86ac8fc371f5ff1f0823c35eb Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 10:29:35 +0900 Subject: [PATCH 002/202] opt dark theme in gesture_help.dart --- flutter/lib/mobile/widgets/gesture_help.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/flutter/lib/mobile/widgets/gesture_help.dart b/flutter/lib/mobile/widgets/gesture_help.dart index 37cc77c8f..bc31ae2c4 100644 --- a/flutter/lib/mobile/widgets/gesture_help.dart +++ b/flutter/lib/mobile/widgets/gesture_help.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:toggle_switch/toggle_switch.dart'; -import '../../models/model.dart'; - class GestureIcons { static const String _family = 'gestureicons'; @@ -79,7 +77,10 @@ class _GestureHelpState extends State { children: [ ToggleSwitch( initialLabelIndex: _selectedIndex, - inactiveBgColor: MyTheme.darkGray, + activeFgColor: Colors.white, + inactiveFgColor: Colors.white60, + activeBgColor: [MyTheme.accent], + inactiveBgColor: Theme.of(context).hintColor, totalSwitches: 2, minWidth: 150, fontSize: 15, @@ -188,7 +189,7 @@ class GestureInfo extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - width: this.width, + width: width, child: Column( children: [ Icon( @@ -199,11 +200,14 @@ class GestureInfo extends StatelessWidget { SizedBox(height: 6), Text(fromText, textAlign: TextAlign.center, - style: TextStyle(fontSize: 9, color: Colors.grey)), + style: + TextStyle(fontSize: 9, color: Theme.of(context).hintColor)), SizedBox(height: 3), Text(toText, textAlign: TextAlign.center, - style: TextStyle(fontSize: 12, color: Colors.black)) + style: TextStyle( + fontSize: 12, + color: Theme.of(context).textTheme.bodySmall?.color)) ], )); } From 4f25b03a10a41adf3220a35944b99cfc6ff6ea61 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 9 Feb 2023 16:54:26 +0800 Subject: [PATCH 003/202] fix CI --- src/flutter.rs | 15 +++++++++++---- src/flutter_ffi.rs | 48 ++++++++++++++++++++++------------------------ 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 2d7d3fb86..bf5746c13 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,5 +1,8 @@ -use crate::ui_session_interface::{io_loop, InvokeUiSession, Session}; -use crate::{client::*, flutter_ffi::EventToUI}; +use crate::{ + client::*, + flutter_ffi::EventToUI, + ui_session_interface::{io_loop, InvokeUiSession, Session}, +}; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::{ bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, @@ -549,11 +552,15 @@ pub mod connection_manager { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); assert!(h.get("name").is_none()); h.insert("name", name); - + if let Some(s) = GLOBAL_EVENT_STREAM.read().unwrap().get(super::APP_TYPE_CM) { s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); } else { - println!("Push event {} failed. No {} event stream found.", name, super::APP_TYPE_CM); + println!( + "Push event {} failed. No {} event stream found.", + name, + super::APP_TYPE_CM + ); }; } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ad0d119d7..a7e32d0b2 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,27 +1,25 @@ -use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char}; -use std::str::FromStr; - -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "android"))] -use std::thread; - -use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; -use serde_json::json; - -use hbb_common::{ - config::{self, LocalConfig, ONLINE, PeerConfig}, - fs, log, -}; -use hbb_common::message_proto::KeyboardMode; -use hbb_common::ResultType; - use crate::{ client::file_trait::FileManager, common::make_fd_to_json, + common::{get_default_sound_input, is_keyboard_mode_supported}, + flutter::{self, SESSIONS}, flutter::{session_add, session_start_}, + ui_interface::{self, *}, +}; +use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; +use hbb_common::{ + config::{self, LocalConfig, PeerConfig, ONLINE}, + fs, log, + message_proto::KeyboardMode, + ResultType, +}; +use serde_json::json; +use std::{ + collections::HashMap, + ffi::{CStr, CString}, + os::raw::c_char, + str::FromStr, }; -use crate::common::{get_default_sound_input, is_keyboard_mode_supported}; -use crate::flutter::{self, SESSIONS}; -use crate::ui_interface::{self, *}; // use crate::hbbs_http::account::AuthResult; @@ -931,7 +929,7 @@ pub fn main_start_dbus_server() { { use crate::dbus::start_dbus_server; // spawn new thread to start dbus server - thread::spawn(|| { + std::thread::spawn(|| { let _ = start_dbus_server(); }); } @@ -1278,7 +1276,7 @@ pub fn main_is_login_wayland() -> SyncReturn { pub fn main_start_pa() { #[cfg(target_os = "linux")] - thread::spawn(crate::ipc::start_pa); + std::thread::spawn(crate::ipc::start_pa); } pub fn main_hide_docker() -> SyncReturn { @@ -1298,7 +1296,7 @@ pub fn cm_start_listen_ipc_thread() { /// * macOS only pub fn main_start_ipc_url_server() { #[cfg(target_os = "macos")] - thread::spawn(move || crate::server::start_ipc_url_server()); + std::thread::spawn(move || crate::server::start_ipc_url_server()); } /// Send a url scheme throught the ipc. @@ -1307,16 +1305,16 @@ pub fn main_start_ipc_url_server() { #[allow(unused_variables)] pub fn send_url_scheme(_url: String) { #[cfg(target_os = "macos")] - thread::spawn(move || crate::ui::macos::handle_url_scheme(_url)); + std::thread::spawn(move || crate::ui::macos::handle_url_scheme(_url)); } #[cfg(target_os = "android")] pub mod server_side { use hbb_common::log; use jni::{ - JNIEnv, objects::{JClass, JString}, sys::jstring, + JNIEnv, }; use crate::start_server; @@ -1327,7 +1325,7 @@ pub mod server_side { _class: JClass, ) { log::debug!("startServer from java"); - thread::spawn(move || start_server(true)); + std::thread::spawn(move || start_server(true)); } #[no_mangle] From fcd1f9b4a3758112098108e563f429a7897576d8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 9 Feb 2023 18:11:32 +0800 Subject: [PATCH 004/202] refactor handle_applicationShouldOpenUntitledFile --- src/flutter.rs | 6 +----- src/platform/macos.rs | 15 +++++++++++++-- src/ui/macos.rs | 18 +++++++++--------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index bf5746c13..f60d9b30e 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -42,11 +42,7 @@ pub extern "C" fn rustdesk_core_main() -> bool { #[cfg(target_os = "macos")] #[no_mangle] pub extern "C" fn handle_applicationShouldOpenUntitledFile() { - hbb_common::log::debug!("icon clicked on finder"); - let x = std::env::args().nth(1).unwrap_or_default(); - if x == "--server" || x == "--cm" { - crate::platform::macos::check_main_window(); - } + crate::platform::macos::handle_applicationShouldOpenUntitledFile(); } #[cfg(windows)] diff --git a/src/platform/macos.rs b/src/platform/macos.rs index c7dbd9b73..b61f51732 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -557,7 +557,7 @@ pub fn hide_dock() { } } -pub fn check_main_window() { +fn check_main_window() -> bool { use sysinfo::{ProcessExt, System, SystemExt}; let mut sys = System::new(); sys.refresh_processes(); @@ -568,11 +568,22 @@ pub fn check_main_window() { .unwrap_or_default(); for (_, p) in sys.processes().iter() { if p.cmd().len() == 1 && p.user_id() == my_uid && p.cmd()[0].contains(&app) { - return; + return true; } } std::process::Command::new("open") .args(["-n", &app]) .status() .ok(); + false +} + +pub fn handle_applicationShouldOpenUntitledFile() { + hbb_common::log::debug!("icon clicked on finder"); + let x = std::env::args().nth(1).unwrap_or_default(); + if x == "--server" || x == "--cm" { + if crate::platform::macos::check_main_window() { + crate::ipc::send_url_scheme("rustdesk:".into()); + } + } } diff --git a/src/ui/macos.rs b/src/ui/macos.rs index f34b7c2c1..c6600608b 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -6,15 +6,15 @@ use cocoa::{ base::{id, nil, YES}, foundation::{NSAutoreleasePool, NSString}, }; +use objc::runtime::Class; use objc::{ class, declare::ClassDecl, msg_send, - runtime::{BOOL, Object, Sel}, + runtime::{Object, Sel, BOOL}, sel, sel_impl, }; -use objc::runtime::Class; -use sciter::{Host, make_args}; +use sciter::{make_args, Host}; use hbb_common::log; @@ -102,7 +102,10 @@ unsafe fn set_delegate(handler: Option>) { sel!(handleMenuItem:), handle_menu_item as extern "C" fn(&mut Object, Sel, id), ); - decl.add_method(sel!(handleEvent:withReplyEvent:), handle_apple_event as extern fn(&Object, Sel, u64, u64)); + decl.add_method( + sel!(handleEvent:withReplyEvent:), + handle_apple_event as extern "C" fn(&Object, Sel, u64, u64), + ); let decl = decl.register(); let delegate: id = msg_send![decl, alloc]; let () = msg_send![delegate, init]; @@ -138,10 +141,7 @@ extern "C" fn application_should_handle_open_untitled_file( if !LAUNCHED { return YES; } - log::debug!("icon clicked on finder"); - if std::env::args().nth(1) == Some("--server".to_owned()) { - crate::platform::macos::check_main_window(); - } + crate::platform::macos::handle_applicationShouldOpenUntitledFile(); let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); let inner = &mut *(inner as *mut DelegateState); (*inner).command(AWAKE); @@ -191,7 +191,7 @@ pub fn handle_url_scheme(url: String) { } } -extern fn handle_apple_event(_this: &Object, _cmd: Sel, event: u64, _reply: u64) { +extern "C" fn handle_apple_event(_this: &Object, _cmd: Sel, event: u64, _reply: u64) { let event = event as *mut Object; let url = fruitbasket::parse_url_event(event); log::debug!("an event was received: {}", url); From c03adf53347ac519e2c4bb4327bbcca6599d06ab Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Thu, 9 Feb 2023 15:30:41 +0330 Subject: [PATCH 005/202] Update fa.rs --- src/lang/fa.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index c206f91ff..8413673a1 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "خودکار"), ("Other Default Options", "سایر گزینه های پیش فرض"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "تماس صوتی"), + ("Text chat", "گفتگو متنی (چت متنی)"), + ("Stop voice call", "توقف تماس صوتی"), ].iter().cloned().collect(); } From 15a8460fcd36a690915fa52f984377a9e9570e65 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Thu, 9 Feb 2023 13:36:48 +0100 Subject: [PATCH 006/202] removed SizedBox --- .../lib/desktop/pages/port_forward_page.dart | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index f513a1c6a..2385813eb 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -179,36 +179,33 @@ class _PortForwardPageState extends State buildTunnelInputCell(context, controller: remotePortController, inputFormatters: portInputFormatter), - SizedBox( - width: _kColumn4Width, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - elevation: 0, side: const BorderSide(color: MyTheme.border)), - onPressed: () async { - int? localPort = int.tryParse(localPortController.text); - int? remotePort = int.tryParse(remotePortController.text); - if (localPort != null && - remotePort != null && - (remoteHostController.text.isEmpty || - remoteHostController.text.trim().isNotEmpty)) { - await bind.sessionAddPortForward( - id: 'pf_${widget.id}', - localPort: localPort, - remoteHost: remoteHostController.text.trim().isEmpty - ? 'localhost' - : remoteHostController.text.trim(), - remotePort: remotePort); - localPortController.clear(); - remoteHostController.clear(); - remotePortController.clear(); - refreshTunnelConfig(); - } - }, - child: Text( - translate('Add'), - ), - ).marginAll(10), - ), + ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, side: const BorderSide(color: MyTheme.border)), + onPressed: () async { + int? localPort = int.tryParse(localPortController.text); + int? remotePort = int.tryParse(remotePortController.text); + if (localPort != null && + remotePort != null && + (remoteHostController.text.isEmpty || + remoteHostController.text.trim().isNotEmpty)) { + await bind.sessionAddPortForward( + id: 'pf_${widget.id}', + localPort: localPort, + remoteHost: remoteHostController.text.trim().isEmpty + ? 'localhost' + : remoteHostController.text.trim(), + remotePort: remotePort); + localPortController.clear(); + remoteHostController.clear(); + remotePortController.clear(); + refreshTunnelConfig(); + } + }, + child: Text( + translate('Add'), + ), + ).marginAll(10), ]), ); } From f7643077d339accf11896d9be4e1d876e0c88f99 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 9 Feb 2023 21:28:42 +0800 Subject: [PATCH 007/202] new tray --- Cargo.lock | 670 +++++++++++++----- Cargo.toml | 13 +- .../macos/Runner.xcodeproj/project.pbxproj | 16 - res/mac-tray-dark-x2.png | Bin 1585 -> 703 bytes res/mac-tray-dark.png | Bin 535 -> 0 bytes res/mac-tray-light-x2.png | Bin 1193 -> 728 bytes res/mac-tray-light.png | Bin 415 -> 0 bytes src/core_main.rs | 25 +- src/flutter.rs | 2 +- src/platform/macos.rs | 8 +- src/tray.rs | 224 ++---- src/ui/macos.rs | 9 +- 12 files changed, 569 insertions(+), 398 deletions(-) delete mode 100644 res/mac-tray-dark.png delete mode 100644 res/mac-tray-light.png diff --git a/Cargo.lock b/Cargo.lock index 83f623ca7..f0f66e287 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,7 +153,7 @@ checksum = "dc120354d1b5ec6d7aaf4876b602def75595937b5e15d356eb554ab5177e08bb" dependencies = [ "clipboard-win", "core-graphics 0.22.3", - "image", + "image 0.23.14", "log", "objc", "objc-foundation", @@ -278,24 +278,24 @@ dependencies = [ [[package]] name = "atk" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +checksum = "39991bc421ddf72f70159011b323ff49b0f783cc676a7287c59453da2e2531cf" dependencies = [ "atk-sys", "bitflags", - "glib 0.15.12", + "glib 0.16.5", "libc", ] [[package]] name = "atk-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148" dependencies = [ - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "system-deps 6.0.3", ] @@ -405,6 +405,12 @@ dependencies = [ "syn", ] +[[package]] +name = "bit_field" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" + [[package]] name = "bitflags" version = "1.3.2" @@ -508,24 +514,25 @@ dependencies = [ [[package]] name = "cairo-rs" -version = "0.15.12" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d" dependencies = [ "bitflags", "cairo-sys-rs", - "glib 0.15.12", + "glib 0.16.5", "libc", + "once_cell", "thiserror", ] [[package]] name = "cairo-sys-rs" -version = "0.15.1" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421" dependencies = [ - "glib-sys 0.15.10", + "glib-sys 0.16.3", "libc", "system-deps 6.0.3", ] @@ -972,7 +979,7 @@ dependencies = [ "alsa", "core-foundation-sys 0.8.3", "coreaudio-rs", - "jni", + "jni 0.19.0", "js-sys", "lazy_static", "libc", @@ -1059,6 +1066,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1131,9 +1144,9 @@ dependencies = [ [[package]] name = "dark-light" -version = "0.2.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413487ef345ab5cdfbf23e66070741217a701bce70f2f397a54221b4f2b6056a" +checksum = "a62007a65515b3cd88c733dd3464431f05d2ad066999a824259d8edc3cf6f645" dependencies = [ "dconf_rs", "detect-desktop-environment", @@ -1712,6 +1725,22 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "exr" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8af5ef47e2ed89d23d0ecbc1b681b30390069de70260937877514377fc24feb" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide 0.6.2", + "smallvec", + "threadpool", + "zune-inflate", +] + [[package]] name = "extend" version = "1.1.2" @@ -1794,6 +1823,19 @@ dependencies = [ "time 0.3.9", ] +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin 0.9.5", +] + [[package]] name = "flutter_rust_bridge" version = "1.61.1" @@ -2040,63 +2082,90 @@ dependencies = [ [[package]] name = "gdk" -version = "0.15.4" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +checksum = "aa9cb33da481c6c040404a11f8212d193889e9b435db2c14fd86987f630d3ce1" dependencies = [ "bitflags", "cairo-rs", "gdk-pixbuf", "gdk-sys", "gio", - "glib 0.15.12", + "glib 0.16.5", "libc", "pango", ] [[package]] name = "gdk-pixbuf" -version = "0.15.11" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05" dependencies = [ "bitflags", "gdk-pixbuf-sys", "gio", - "glib 0.15.12", + "glib 0.16.5", "libc", ] [[package]] name = "gdk-pixbuf-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016" dependencies = [ - "gio-sys 0.15.10", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "system-deps 6.0.3", ] [[package]] name = "gdk-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", - "gio-sys 0.15.10", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "pango-sys", "pkg-config", "system-deps 6.0.3", ] +[[package]] +name = "gdkwayland-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4511710212ed3020b61a8622a37aa6f0dd2a84516575da92e9b96928dcbe83ba" +dependencies = [ + "gdk-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "pkg-config", + "system-deps 6.0.3", +] + +[[package]] +name = "gdkx11-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa2bf8b5b8c414bc5d05e48b271896d0fd3ddb57464a3108438082da61de6af" +dependencies = [ + "gdk-sys", + "glib-sys 0.16.3", + "libc", + "system-deps 6.0.3", + "x11 2.20.1", +] + [[package]] name = "generic-array" version = "0.14.6" @@ -2124,8 +2193,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", ] [[package]] @@ -2136,34 +2217,24 @@ checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" [[package]] name = "gio" -version = "0.15.12" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092" dependencies = [ "bitflags", "futures-channel", "futures-core", "futures-io", - "gio-sys 0.15.10", - "glib 0.15.12", + "futures-util", + "gio-sys", + "glib 0.16.5", "libc", "once_cell", + "pin-project-lite", + "smallvec", "thiserror", ] -[[package]] -name = "gio-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" -dependencies = [ - "glib-sys 0.15.10", - "gobject-sys 0.15.10", - "libc", - "system-deps 6.0.3", - "winapi 0.3.9", -] - [[package]] name = "gio-sys" version = "0.16.3" @@ -2196,26 +2267,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "glib" -version = "0.15.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "glib-macros 0.15.11", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", - "libc", - "once_cell", - "smallvec", - "thiserror", -] - [[package]] name = "glib" version = "0.16.5" @@ -2228,7 +2279,7 @@ dependencies = [ "futures-executor", "futures-task", "futures-util", - "gio-sys 0.16.3", + "gio-sys", "glib-macros 0.16.3", "glib-sys 0.16.3", "gobject-sys 0.16.3", @@ -2254,21 +2305,6 @@ dependencies = [ "syn", ] -[[package]] -name = "glib-macros" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" -dependencies = [ - "anyhow", - "heck 0.4.0", - "proc-macro-crate 1.2.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "glib-macros" version = "0.16.3" @@ -2294,16 +2330,6 @@ dependencies = [ "system-deps 1.3.2", ] -[[package]] -name = "glib-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" -dependencies = [ - "libc", - "system-deps 6.0.3", -] - [[package]] name = "glib-sys" version = "0.16.3" @@ -2331,17 +2357,6 @@ dependencies = [ "system-deps 1.3.2", ] -[[package]] -name = "gobject-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" -dependencies = [ - "glib-sys 0.15.10", - "libc", - "system-deps 6.0.3", -] - [[package]] name = "gobject-sys" version = "0.16.3" @@ -2370,7 +2385,7 @@ dependencies = [ "gstreamer-sys", "libc", "muldiv", - "num-rational", + "num-rational 0.3.2", "once_cell", "paste", "pretty-hex", @@ -2488,9 +2503,9 @@ dependencies = [ [[package]] name = "gtk" -version = "0.15.5" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +checksum = "e4d3507d43908c866c805f74c9dd593c0ce7ba5c38e576e41846639cdcd4bee6" dependencies = [ "atk", "bitflags", @@ -2500,7 +2515,7 @@ dependencies = [ "gdk", "gdk-pixbuf", "gio", - "glib 0.15.12", + "glib 0.16.5", "gtk-sys", "gtk3-macros", "libc", @@ -2511,17 +2526,17 @@ dependencies = [ [[package]] name = "gtk-sys" -version = "0.15.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3" dependencies = [ "atk-sys", "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", - "gio-sys 0.15.10", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "pango-sys", "system-deps 6.0.3", @@ -2529,9 +2544,9 @@ dependencies = [ [[package]] name = "gtk3-macros" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" +checksum = "8cfd6557b1018b773e43c8de9d0d13581d6b36190d0501916cbec4731db5ccff" dependencies = [ "anyhow", "proc-macro-crate 1.2.1", @@ -2560,6 +2575,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2781,10 +2805,29 @@ dependencies = [ "byteorder", "color_quant", "num-iter", - "num-rational", + "num-rational 0.3.2", "num-traits 0.2.15", - "png", - "tiff", + "png 0.16.8", + "tiff 0.6.1", +] + +[[package]] +name = "image" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder 0.3.0", + "num-rational 0.4.1", + "num-traits 0.2.15", + "png 0.17.7", + "scoped_threadpool", + "tiff 0.8.1", ] [[package]] @@ -2915,6 +2958,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -2936,6 +2993,15 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.60" @@ -2955,6 +3021,17 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "keyboard-types" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7668b7cff6a51fe61cdde64cd27c8a220786f399501b57ebe36f7d8112fd68" +dependencies = [ + "bitflags", + "serde 1.0.149", + "unicode-segmentation", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2968,12 +3045,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] -name = "libappindicator" -version = "0.7.1" +name = "lebe" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libappindicator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e1edfdc9b0853358306c6dfb4b77c79c779174256fe93d80c0b5ebca451a2f" dependencies = [ - "glib 0.15.12", + "glib 0.16.5", "gtk", "gtk-sys", "libappindicator-sys", @@ -2982,9 +3065,9 @@ dependencies = [ [[package]] name = "libappindicator-sys" -version = "0.7.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa" +checksum = "08fcb2bea89cee9613982501ec83eaa2d09256b24540ae463c52a28906163918" dependencies = [ "gtk-sys", "libloading", @@ -3085,6 +3168,25 @@ dependencies = [ "walkdir", ] +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11 2.20.1", +] + [[package]] name = "link-cplusplus" version = "1.0.7" @@ -3340,12 +3442,41 @@ dependencies = [ "glob", ] +[[package]] +name = "muda" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66365a21dc5e322c6b6ba25c735d00153c57dd2eb377926aa50e3caf547b6f6" +dependencies = [ + "cocoa", + "crossbeam-channel", + "gdk", + "gdk-pixbuf", + "gtk", + "keyboard-types", + "libxdo", + "objc", + "once_cell", + "png 0.17.7", + "thiserror", + "windows-sys 0.45.0", +] + [[package]] name = "muldiv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + [[package]] name = "ndk" version = "0.5.0" @@ -3616,6 +3747,17 @@ dependencies = [ "num-traits 0.2.15", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits 0.2.15", +] + [[package]] name = "num-traits" version = "0.1.43" @@ -3728,7 +3870,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" dependencies = [ - "jni", + "jni 0.19.0", "ndk 0.6.0", "ndk-context", "num-derive", @@ -3747,9 +3889,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "openssl-probe" @@ -3783,20 +3925,15 @@ version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" -[[package]] -name = "padlock" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c10569378a1dacd9f30dbe7ae49e054d2c45dc2f8ee49899903e09c3924e8b6f" - [[package]] name = "pango" -version = "0.15.10" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94" dependencies = [ "bitflags", - "glib 0.15.12", + "gio", + "glib 0.16.5", "libc", "once_cell", "pango-sys", @@ -3804,12 +3941,12 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f" dependencies = [ - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "system-deps 6.0.3", ] @@ -4005,6 +4142,18 @@ dependencies = [ "miniz_oxide 0.3.7", ] +[[package]] +name = "png" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +dependencies = [ + "bitflags", + "crc32fast", + "flate2", + "miniz_oxide 0.6.2", +] + [[package]] name = "polling" version = "2.5.1" @@ -4547,7 +4696,7 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", + "spin 0.5.2", "untrusted", "web-sys", "winapi 0.3.9", @@ -4690,15 +4839,13 @@ dependencies = [ "flutter_rust_bridge", "flutter_rust_bridge_codegen", "fruitbasket", - "glib 0.16.5", - "gtk", "hbb_common", "hound", + "image 0.24.5", "impersonate_system", "include_dir", - "jni", + "jni 0.19.0", "lazy_static", - "libappindicator", "libc", "libpulse-binding", "libpulse-simple-binding", @@ -4730,7 +4877,8 @@ dependencies = [ "sys-locale", "sysinfo", "system_shutdown", - "tray-item", + "tao", + "tray-icon", "trayicon", "url", "uuid", @@ -4868,6 +5016,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "scopeguard" version = "1.1.0" @@ -4889,7 +5043,7 @@ dependencies = [ "gstreamer-video", "hbb_common", "hwcodec", - "jni", + "jni 0.19.0", "lazy_static", "libc", "log", @@ -5127,6 +5281,12 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +[[package]] +name = "simd-adler32" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a5df39617d7c8558154693a1bb8157a4aab8179209540cc0b10e5dc24e0b18" + [[package]] name = "simple_rc" version = "0.1.0" @@ -5217,6 +5377,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc" +dependencies = [ + "lock_api", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -5399,6 +5568,61 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "tao" +version = "0.17.0" +source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf" +dependencies = [ + "bitflags", + "cairo-rs", + "cc", + "cocoa", + "core-foundation 0.9.3", + "core-graphics 0.22.3", + "crossbeam-channel", + "dispatch", + "gdk", + "gdk-pixbuf", + "gdk-sys", + "gdkwayland-sys", + "gdkx11-sys", + "gio", + "glib 0.16.5", + "glib-sys 0.16.3", + "gtk", + "image 0.24.5", + "instant", + "jni 0.20.0", + "lazy_static", + "libc", + "log", + "ndk 0.6.0", + "ndk-context", + "ndk-sys 0.3.0", + "objc", + "once_cell", + "parking_lot 0.12.1", + "png 0.17.7", + "raw-window-handle 0.5.0", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "uuid", + "windows 0.44.0", + "windows-implement", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.0" +source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tap" version = "1.0.1" @@ -5509,11 +5733,22 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" dependencies = [ - "jpeg-decoder", + "jpeg-decoder 0.1.22", "miniz_oxide 0.4.4", "weezl", ] +[[package]] +name = "tiff" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" +dependencies = [ + "flate2", + "jpeg-decoder 0.3.0", + "weezl", +] + [[package]] name = "time" version = "0.1.45" @@ -5698,21 +5933,22 @@ dependencies = [ ] [[package]] -name = "tray-item" -version = "0.7.1" +name = "tray-icon" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0914b62e00e8f51241806cb9f9c4ea6b10c75d94cae02c89278de6f4b98c7d0f" +checksum = "d62801a4da61bb100b8d3174a5a46fed7b6ea03cc2ae93ee7340793b09a94ce3" dependencies = [ "cocoa", "core-graphics 0.22.3", - "gtk", + "crossbeam-channel", + "dirs-next", "libappindicator", - "libc", + "muda", "objc", - "objc-foundation", - "objc_id", - "padlock", - "winapi 0.3.9", + "once_cell", + "png 0.17.7", + "thiserror", + "windows-sys 0.45.0", ] [[package]] @@ -5811,9 +6047,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" dependencies = [ "getrandom", ] @@ -6242,6 +6478,39 @@ dependencies = [ "windows_x86_64_msvc 0.34.0", ] +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-service" version = "0.4.0" @@ -6287,19 +6556,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.0", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.1", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" @@ -6327,9 +6620,9 @@ checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" @@ -6357,9 +6650,9 @@ checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" @@ -6387,9 +6680,9 @@ checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" @@ -6417,15 +6710,15 @@ checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" @@ -6453,9 +6746,9 @@ checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] name = "winit" @@ -6566,12 +6859,12 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.20.1" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" dependencies = [ - "lazy_static", "libc", + "once_cell", "pkg-config", ] @@ -6703,6 +6996,15 @@ dependencies = [ "libc", ] +[[package]] +name = "zune-inflate" +version = "0.2.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c473377c11c4a3ac6a2758f944cd336678e9c977aa0abf54f6450cf77e902d6d" +dependencies = [ + "simd-adler32", +] + [[package]] name = "zvariant" version = "3.9.0" diff --git a/Cargo.toml b/Cargo.toml index b315024e9..9588d10b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,6 @@ arboard = "2.0" system_shutdown = "3.0.0" [target.'cfg(target_os = "windows")'.dependencies] -#systray = { git = "https://github.com/open-trade/systray-rs" } trayicon = { git = "https://github.com/open-trade/trayicon-rs", features = ["winit"] } winit = "0.26" winapi = { version = "0.3", features = ["winuser"] } @@ -104,11 +103,15 @@ dispatch = "0.2" core-foundation = "0.9" core-graphics = "0.22" include_dir = "0.7.2" -tray-item = "0.7" # looks better than trayicon -dark-light = "0.2" +dark-light = "1.0" fruitbasket = "0.10.0" objc_id = "0.1.1" +[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies] +tray-icon = "0.4" +tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" } +image = "0.24" + [target.'cfg(target_os = "linux")'.dependencies] psimple = { package = "libpulse-simple-binding", version = "2.25" } pulse = { package = "libpulse-binding", version = "2.26" } @@ -118,9 +121,6 @@ mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } dbus = "0.9" dbus-crossroads = "0.5" -gtk = "0.15" -libappindicator = "0.7" -glib = "0.16.5" backtrace = "0.3" [target.'cfg(target_os = "android")'.dependencies] @@ -157,7 +157,6 @@ identifier = "com.carriez.rustdesk" icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"] deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "curl", "libvdpau1", "libva2"] osx_minimum_system_version = "10.14" -resources = ["res/mac-tray-light.png","res/mac-tray-dark.png", "res/mac-tray-light-x2.png","res/mac-tray-dark-x2.png"] #https://github.com/johnthagen/min-sized-rust [profile.release] diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 066560203..0019335ef 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -26,10 +26,6 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */; }; - 7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */; }; - 7E881462296E98EE00A0C54F /* mac-tray-light-x2.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */; }; - 7E881464296E991200A0C54F /* mac-tray-dark-x2.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */; }; 84010BA8292CF66600152837 /* liblibrustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (Weak, ); }; }; 84010BA9292CF68300152837 /* liblibrustdesk.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; }; @@ -78,10 +74,6 @@ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light.png"; path = "../../res/mac-tray-light.png"; sourceTree = ""; }; - 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark.png"; path = "../../res/mac-tray-dark.png"; sourceTree = ""; }; - 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light-x2.png"; path = "../../res/mac-tray-light-x2.png"; sourceTree = ""; }; - 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark-x2.png"; path = "../../res/mac-tray-dark-x2.png"; sourceTree = ""; }; 84010BA7292CF66600152837 /* liblibrustdesk.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = liblibrustdesk.dylib; path = ../../target/release/liblibrustdesk.dylib; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; @@ -135,10 +127,6 @@ 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( - 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */, - 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */, - 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */, - 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */, 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, @@ -265,12 +253,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7E881462296E98EE00A0C54F /* mac-tray-light-x2.png in Resources */, - 7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */, - 7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */, 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - 7E881464296E991200A0C54F /* mac-tray-dark-x2.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/res/mac-tray-dark-x2.png b/res/mac-tray-dark-x2.png index bdd48ad15ade67946a7c45b5ff1896ce96878ca3..595b850aef971e9e756aa55222318de018782cad 100644 GIT binary patch literal 703 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sT>(BJu0UD}24rMpfJ_Mq35WoM z3uH@5N+OFxxDYiES-2Lspt!g=L`qgx7A^>3NJ~pY7(jC%YzP_1Z50M|jc!ShUogX; zPhSLOC0_OOy}g+iuK4}l*`4VIlcIDUmnQ|Oi*B36aqQ@-1wBRP2P)&eYL+XTdN43B zZufL?4DmQVb=vi5O$GuE?H>AC3JaK$)?5GoU!AQiX~whbJLkEboF}(1XWW=Ln}sRR z=X_+|LH08R&9aXcTQ0c%j>A-~_v}yZMZs=~>+Sj`K4q~oZ?swZF20ws{#9mO7xS6p zv*M-xf7X8cr|q%C9{xh}55NDsj0jYI_dLJWc-Gc`{^~EpCq?P|-`<|ox1&a!|7`f7 z5dVh?i~dK=6|`r*x4*bzUT@8U#6LGryMOdJ5%^R4*!DTsJQy9WZr-qC)s$0`2i#6F zNleqSH+`bDAZK-ja!}j4ntRNrjlzREhrX;Y`fbcAG9%*mI0B4JkNw{^9Q zbb@w#(7L-<*v-B;1yA-gmehKveC*NN{)O{p|Gi}R{xtf`KkHn#D@h0RldLx9+-!7w z9X)+YOzNDKzc0zH%gZ+MSKFQXka5QH$Ey}6-w8I}r{h{XueFVdQ&MBb@04s+K-~a#s delta 1579 zcmV+`2Gse#1+ff}8Gi-<00374`G)`i010qNS#tmY8$kd78$kiGsWprM000?uMObuG zZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs000HPNkl3Nt`OY^(%nZK-gTaul z>(3EUH#08<&EDYq5v*4^LbTO&%|Oe%kR0V(f}fnNYm8R)CMAY2S7If zs-T;TM05zizJGW;K3<6r&jEx&p`fN|pD^<#0F9NLv;eo6dB3J-kN9|bVqw<}4A=-Tx3^l@2z(SHEi+S)cV^Iia2Rh-E&oKB}- zP9~F+vJDE(0mCrf0B{(9T19i^7%P2MmvC~0K5d??YwggI*#)~ zB9Vv|>wh>Bi8M`5Pp<}W)v~Nh`N!_M1Ey(y%FH`G8lPn5-MX&t85$b;N2WnA7;Mya z{dH!ZH)qbA-e@#>y_hx>3N2Gr^#n4h2?N+A3%c#^FI*Lrg%Ia z_s~X~tohRbf_Z;mC!!^})MI89vTb_@yqW((2!HWvt;~nR;YUx^)1cw70jn z5r5Hg$^LiMYRu;`Dye(gFpPzY<2XH#+l<`OH0{gEeaVqZrH%l&B_UOas7E296_S1T z%C7T%7#|!G=G``kdlx#Dv(=&%5FXXFBAYWyPpNMHj+LjH9D#&a)%Ap&E5%y$q#`B_P6R3V}(lAXTn z!;@E5$%u&PiXyw_0$3Le25VK3n;|B_f5H_XBuBvdLs15I6v20~i<>xIskU6cG@Ng)j_bu3;G85z$*7nty$pE5*#D2#(|I25@thFv`q5LI~O9$`j#m_)!2q zGxM7snoXwD>0P-WGvjS!WMs@RjI98^MkYV{1HdXF#Bi~mOw%-7*If@_OxN|pvO}!3 zwY5`Gl&t_hg7@O_uF=uanQdpm+9)%3HZ(N+)ZgDfRl@w^%zOfOHsC1$$A18vb{wbA zah##Cv9XD)w%K*v2!LmZXcd50P+5Zbn-F3-GpCCNAVb#tDFDr7q`B%vHLBbb0G3;p zbv6G_&$WLK=7H*!=UryrAcVL&`+%q0=+S&N*(!uMTIhiE5DJAtK~+_MheXk-ia12H zB_5CODJGB}LaM5+!TmLV6Mw*lcszcrl*r2*_|u;sPh0}9ZuW#Ng!CM+EbAPAk5M}1 z@XI8C?LvqZGUfq)pBlUm|L@UqOaj=OPN#S0Kcr-+_+yX7ix8Jnz diff --git a/res/mac-tray-dark.png b/res/mac-tray-dark.png deleted file mode 100644 index a98fe63b0930e9e9358059dd6661e1eaadfd73fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 535 zcmV+y0_gpTP);W&wBJ!Z>At9 zUc^FsksPeMS(97nWkF(EkrOS0N(DabN?WuIt^u z;B$9>BO)IF-2JYIY?;}!s;a6qO@pf5EX(qpWLefTv#(Lx$7c3;JRYAdYaxWNWoAb} z$KAVXW;+plz6u6tn&tq=09Dl;1@1-p*Q?-}zD4K0a`9En?)qsBjH-T){F@3$l=pv; zMY;QeU%40(07T^Wx&pwR$p1ru_faxmMTQV^;Azy&K+S9)FyL%9!b_E9*>y8}3tX9n zOjTcryOXBrgCt2#i=y}$Ldct@c_|`8cmJZQ;_kPAClL&=S5?&uiI=I<>0A|&k3hQS z9gbS9*2Cd&_-kQlM5G6_SAYiW&0&bPajtf|eIrSdZJ_V&*MQ$5Qn>q|X_|w{WO5q& Z{{Yw=yte_XgkS&w002ovPDHLkV1j~<^7a4# diff --git a/res/mac-tray-light-x2.png b/res/mac-tray-light-x2.png index 253450ecbc102a345187896f2b65ab1900d8fcac..2e27118884994f5573334acc2cd962e373903619 100644 GIT binary patch literal 728 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sGXs1=T!HleK?$zPvV;H~XH*j8 z7tHYa&;1DkuOch-WyHd`et&qoYkO|8lDq`p_pdk39xKpMpWN#&eCy<@nHdS@j~C6& zJm4{9{Q_xkK2AHGgg-)V!0xPnYml1&)oC>@L8Y!wu-I= z_u`*gZa0|f#k4^q$y}Rv;>*<62@Td2S10A&2)BOWvQDx4`gf<#&MQyrhOYj%Nqwqu z&Nj`LN4C{VmHpUzr}bp-(~L}e`MA@|6so{@4lW7dl#JlIeizW zt$}C3o_|rb0={}jm6@fquWtU-sqr^nE&Jj6g}>`>{yC!d??LRx-~UwaNj7*LS^R_f ze^w3i4fE%rkHjN?vWgsO39#7s=#7BM&+iKiszp;@eEP+$HD4!p!bhuyQ=U(r_Fk9e zjo%#7l%L2kx5d2YK=0>9zGvateD7HPALMdj}EVtg> zvr;tv;7z;d!FfL?-v9bXd0tiS(&!s=O6vm`_vT+-H~Yg%fA+_2v3KX`=YLIo_mD4e k&&g9$GD7;yH~#%#KWoL|&dIu_pj5!%>FVdQ&MBb@03sKs`Tzg` delta 1184 zcmV;R1Yi5u1*r*;8Gi-<00374`G)`i010qNS#tmY8$kd78$kiGsWprM000?uMObuG zZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs000CwNklZK`teW5`?A@Y>|d7 zW>hM)X;SYUCncSKi|1VDdS>o9&->nUryrcnJ^%B*|L1+*^L{+{v`L}_ZUr6$`hk@| z53m^MNbgsHzkng&81Mt|9q@az6}L4JKo{^d@FZruEuxGgz+T`(U?j?gq|j@jq}L=> zRUTuK-jcK=)PGNic&4NmOE#Y6qNL|T0`Lx@6~HIJT~XZ}&RXC{k1%m)Heh3Qy37MC z^9qZ4U@PzrFtbTRAq)bmfKiV)uLrgR?=!vTPXp_5nuo?c@Hp@}Fryq_E=~gvVLjkx zssL`oUZ9T2@*u{6VXT+ulHV9n=7CAm=qyP`Bd5SINq-+n+9Ih}Qj#fqnWT-9-j!6T zdu}Mz=S`Pg>9cFH_p-uQB8*ohy(DQ)frVXeQXg~pMzO44vn z{S`@_l7FtP16zTco$vhtU`Oz5;inro2+YsXbO4)SVja93m9IoJX$aOgIH z1+WCT+gbQ-;I}wmZq7P&_W|=0U>&~0$QZCE(o_gvq;(aM0P7Oq9%sS(fOD~~0;pg? zc<4{CGpvV$aekus#;IGG0KHD};Yc$v3^;YYZGVy~6mQ(F05<@oq%a)Z2>j#FjU>QL z4&gAn5~k)FiP3@0O7wusrlr@#{d&wK!!2d>LXERb{| z6X~F&b3%4->aR{0Te1U#a~N z`zPOO;Oo>_9<}U68CmWsX8uI|k1@G_uX!jhJwJ~9BYGlxBeWYs%$Lbd;LE9Qgnta7 zg}~4Fl1^oD6K8}c8Qimf_#n!3pV0000JM1D&kPZ zKeHVam-;#MTs&TCZ+{R-Lh?&;a)L@m=G9&GM*UE~)H`)gt!3k5V>|@){2$-y+8~0W zdNL$2#iKeI6BOuU8>sV(E^q{#2YVSP16#l=Fa^|r<8D3DAigiz5&$Mfy_$ zoJNjHPI3j|bip%rV7MRU2wc{ZzX_-%;nX@js(a3259AKP&(M002ov JPDHLkV1ggSrT_o{ diff --git a/src/core_main.rs b/src/core_main.rs index 0af7026e9..e2f3f80e0 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -164,9 +164,6 @@ pub fn core_main() -> Option> { #[cfg(feature = "with_rc")] hbb_common::allow_err!(crate::rc::extract_resources(&args[1])); return None; - } else if args[0] == "--tray" { - crate::tray::start_tray(); - return None; } else if args[0] == "--portable-service" { crate::platform::elevate_or_run_as_system( click_setup, @@ -183,34 +180,24 @@ pub fn core_main() -> Option> { std::fs::remove_file(&args[1]).ok(); return None; } + } else if args[0] == "--tray" { + crate::tray::start_tray(); + return None; } else if args[0] == "--service" { log::info!("start --service"); crate::start_os_service(); return None; } else if args[0] == "--server" { log::info!("start --server with user {}", crate::username()); - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "linux", target_os = "windows"))] { crate::start_server(true); return None; } #[cfg(target_os = "macos")] - { - std::thread::spawn(move || crate::start_server(true)); - crate::platform::macos::hide_dock(); - crate::ui::macos::make_tray(); - return None; - } - #[cfg(target_os = "linux")] { let handler = std::thread::spawn(move || crate::start_server(true)); - // Show the tray in linux only when current user is a normal user - // [Note] - // As for GNOME, the tray cannot be shown in user's status bar. - // As for KDE, the tray can be shown without user's theme. - if !crate::platform::is_root() { - crate::tray::start_tray(); - } + crate::tray::start_tray(); // prevent server exit when encountering errors from tray hbb_common::allow_err!(handler.join()); } @@ -349,6 +336,6 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option bool { #[cfg(target_os = "macos")] #[no_mangle] pub extern "C" fn handle_applicationShouldOpenUntitledFile() { - crate::platform::macos::handle_applicationShouldOpenUntitledFile(); + crate::platform::macos::handle_application_should_open_untitled_file(); } #[cfg(windows)] diff --git a/src/platform/macos.rs b/src/platform/macos.rs index b61f51732..0c8c51455 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -17,7 +17,7 @@ use core_graphics::{ display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo}, window::{kCGWindowName, kCGWindowOwnerPID}, }; -use hbb_common::{bail, log}; +use hbb_common::{allow_err, bail, log}; use include_dir::{include_dir, Dir}; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; @@ -578,12 +578,12 @@ fn check_main_window() -> bool { false } -pub fn handle_applicationShouldOpenUntitledFile() { +pub fn handle_application_should_open_untitled_file() { hbb_common::log::debug!("icon clicked on finder"); let x = std::env::args().nth(1).unwrap_or_default(); - if x == "--server" || x == "--cm" { + if x == "--server" || x == "--cm" || x == "--tray" { if crate::platform::macos::check_main_window() { - crate::ipc::send_url_scheme("rustdesk:".into()); + allow_err!(crate::ipc::send_url_scheme("rustdesk:".into())); } } } diff --git a/src/tray.rs b/src/tray.rs index e41a616de..b449bbbd3 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,11 +1,5 @@ -#[cfg(any(target_os = "linux", target_os = "windows"))] +#[cfg(any(target_os = "windows"))] use super::ui_interface::get_option_opt; -#[cfg(target_os = "linux")] -use hbb_common::log::{debug, error, info}; -#[cfg(target_os = "linux")] -use libappindicator::AppIndicator; -#[cfg(target_os = "linux")] -use std::env::temp_dir; #[cfg(target_os = "windows")] use std::sync::{Arc, Mutex}; #[cfg(target_os = "windows")] @@ -83,119 +77,10 @@ pub fn start_tray() { }); } -/// Start a tray icon in Linux -/// -/// [Block] -/// This function will block current execution, show the tray icon and handle events. -#[cfg(target_os = "linux")] -pub fn start_tray() { - use std::time::Duration; - - use glib::{clone, Continue}; - use gtk::traits::{GtkMenuItemExt, MenuShellExt, WidgetExt}; - - info!("configuring tray"); - // init gtk context - if let Err(err) = gtk::init() { - error!("Error when starting the tray: {}", err); - return; - } - if let Some(mut appindicator) = get_default_app_indicator() { - let mut menu = gtk::Menu::new(); - let stoped = is_service_stopped(); - // start/stop service - let label = if stoped { - crate::client::translate("Start Service".to_owned()) - } else { - crate::client::translate("Stop service".to_owned()) - }; - let menu_item_service = gtk::MenuItem::with_label(label.as_str()); - menu_item_service.connect_activate(move |_| { - let _lock = crate::ui_interface::SENDER.lock().unwrap(); - change_service_state(); - }); - menu.append(&menu_item_service); - // show tray item - menu.show_all(); - appindicator.set_menu(&mut menu); - // start event loop - info!("Setting tray event loop"); - // check the connection status for every second - glib::timeout_add_local( - Duration::from_secs(1), - clone!(@strong menu_item_service as item => move || { - let _lock = crate::ui_interface::SENDER.lock().unwrap(); - update_tray_service_item(&item); - // continue to trigger the next status check - Continue(true) - }), - ); - gtk::main(); - } else { - error!("Tray process exit now"); - } -} - -#[cfg(target_os = "linux")] -fn change_service_state() { - if is_service_stopped() { - debug!("Now try to start service"); - crate::ipc::set_option("stop-service", ""); - } else { - debug!("Now try to stop service"); - crate::ipc::set_option("stop-service", "Y"); - } -} - -#[cfg(target_os = "linux")] -#[inline] -fn update_tray_service_item(item: >k::MenuItem) { - use gtk::traits::GtkMenuItemExt; - - if is_service_stopped() { - item.set_label(&crate::client::translate("Start Service".to_owned())); - } else { - item.set_label(&crate::client::translate("Stop service".to_owned())); - } -} - -#[cfg(target_os = "linux")] -fn get_default_app_indicator() -> Option { - use libappindicator::AppIndicatorStatus; - use std::io::Write; - - let icon = include_bytes!("../res/icon.png"); - // appindicator does not support icon buffer, so we write it to tmp folder - let mut icon_path = temp_dir(); - icon_path.push("RustDesk"); - icon_path.push("rustdesk.png"); - match std::fs::File::create(icon_path.clone()) { - Ok(mut f) => { - f.write_all(icon).unwrap(); - // set .png icon file to be writable - // this ensures successful file rewrite when switching between x11 and wayland. - let mut perm = f.metadata().unwrap().permissions(); - if perm.readonly() { - perm.set_readonly(false); - f.set_permissions(perm).unwrap(); - } - } - Err(err) => { - error!("Error when writing icon to {:?}: {}", icon_path, err); - return None; - } - } - debug!("write temp icon complete"); - let mut appindicator = AppIndicator::new("RustDesk", icon_path.to_str().unwrap_or("rustdesk")); - appindicator.set_label("RustDesk", "A remote control software."); - appindicator.set_status(AppIndicatorStatus::Active); - Some(appindicator) -} - /// Check if service is stoped. /// Return [`true`] if service is stoped, [`false`] otherwise. #[inline] -#[cfg(any(target_os = "linux", target_os = "windows"))] +#[cfg(any(target_os = "windows"))] fn is_service_stopped() -> bool { if let Some(v) = get_option_opt("stop-service") { v == "Y" @@ -204,47 +89,68 @@ fn is_service_stopped() -> bool { } } -#[cfg(target_os = "macos")] -pub fn make_tray() { - extern "C" { - fn BackingScaleFactor() -> f32; - } - let f = unsafe { BackingScaleFactor() }; - use tray_item::TrayItem; - let mode = dark_light::detect(); - let icon_path = match mode { - dark_light::Mode::Dark => { - // still show big overflow icon in my test, so still use x1 png. - // let's do it with objc with svg support later. - // or use another tray crate, or find out in tauri (it has tray support) - if f > 2. { - "mac-tray-light-x2.png" - } else { - "mac-tray-light.png" - } - } - dark_light::Mode::Light => { - if f > 2. { - "mac-tray-dark-x2.png" - } else { - "mac-tray-dark.png" - } - } - }; - if let Ok(mut tray) = TrayItem::new(&crate::get_app_name(), icon_path) { - tray.add_label(&format!( - "{} {}", - crate::get_app_name(), - crate::lang::translate("Service is running".to_owned()) - )) - .ok(); +/// Start a tray icon in Linux +/// +/// [Block] +/// This function will block current execution, show the tray icon and handle events. +#[cfg(target_os = "linux")] +pub fn start_tray() {} - let inner = tray.inner_mut(); - inner.add_quit_item(&crate::lang::translate("Quit".to_owned())); - inner.display(); - } else { - loop { - std::thread::sleep(std::time::Duration::from_secs(3)); - } - } +#[cfg(target_os = "macos")] +pub fn start_tray() { + use hbb_common::{allow_err, log}; + allow_err!(make_tray()); +} + +#[cfg(target_os = "macos")] +pub fn make_tray() -> hbb_common::ResultType<()> { + // https://github.com/tauri-apps/tray-icon/blob/dev/examples/tao.rs + use hbb_common::anyhow::Context; + use tao::event_loop::{ControlFlow, EventLoopBuilder}; + use tray_icon::{TrayEvent, TrayIconBuilder}; + let mode = dark_light::detect(); + const LIGHT: &[u8] = include_bytes!("../res/mac-tray-light-x2.png"); + const DARK: &[u8] = include_bytes!("../res/mac-tray-dark-x2.png"); + let icon = match mode { + dark_light::Mode::Dark => DARK, + _ => LIGHT, + }; + let (icon_rgba, icon_width, icon_height) = { + let image = image::load_from_memory(icon) + .context("Failed to open icon path")? + .into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) + }; + let icon = tray_icon::icon::Icon::from_rgba(icon_rgba, icon_width, icon_height) + .context("Failed to open icon")?; + + let event_loop = EventLoopBuilder::new().build(); + + let _tray_icon = Some( + TrayIconBuilder::new() + .with_tooltip(format!( + "{} {}", + crate::get_app_name(), + crate::lang::translate("Service is running".to_owned()) + )) + .with_icon(icon) + .build()?, + ); + + let tray_channel = TrayEvent::receiver(); + let mut docker_hiden = false; + + event_loop.run(move |_event, _, control_flow| { + if !docker_hiden { + crate::platform::macos::hide_dock(); + docker_hiden = true; + } + *control_flow = ControlFlow::Poll; + + if tray_channel.try_recv().is_ok() { + crate::platform::macos::handle_application_should_open_untitled_file(); + } + }); } diff --git a/src/ui/macos.rs b/src/ui/macos.rs index c6600608b..8a1fc990c 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -141,7 +141,7 @@ extern "C" fn application_should_handle_open_untitled_file( if !LAUNCHED { return YES; } - crate::platform::macos::handle_applicationShouldOpenUntitledFile(); + crate::platform::macos::handle_application_should_open_untitled_file(); let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); let inner = &mut *(inner as *mut DelegateState); (*inner).command(AWAKE); @@ -258,10 +258,3 @@ pub fn show_dock() { NSApp().setActivationPolicy_(NSApplicationActivationPolicyRegular); } } - -pub fn make_tray() { - unsafe { - set_delegate(None); - } - crate::tray::make_tray(); -} From 1f5d68ef224ccdbb2ba00eed37419156ed640615 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 22:55:56 +0900 Subject: [PATCH 008/202] workaround for https://github.com/rustdesk/rustdesk/issues/3131 --- flutter/lib/mobile/pages/remote_page.dart | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index c4b07b375..853f3168c 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -228,13 +228,18 @@ class _RemotePageState extends State { return false; }, child: getRawPointerAndKeyBody(Scaffold( - // resizeToAvoidBottomInset: true, + // workaround for https://github.com/rustdesk/rustdesk/issues/3131 + floatingActionButtonLocation: hideKeyboard + ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) + : null, floatingActionButton: !showActionButton ? null : FloatingActionButton( mini: !hideKeyboard, child: Icon( - hideKeyboard ? Icons.expand_more : Icons.expand_less), + hideKeyboard ? Icons.expand_more : Icons.expand_less, + color: Colors.white, + ), backgroundColor: MyTheme.accent, onPressed: () { setState(() { @@ -1134,3 +1139,16 @@ void sendPrompt(bool isMac, String key) { gFFI.inputModel.ctrl = old; } } + +class FABLocation extends FloatingActionButtonLocation { + FloatingActionButtonLocation location; + double offsetX; + double offsetY; + FABLocation(this.location, this.offsetX, this.offsetY); + + @override + Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { + final offset = location.getOffset(scaffoldGeometry); + return Offset(offset.dx + offsetX, offset.dy + offsetY); + } +} From 2a0c9699e8bf7c2393fcd863b256c976cde8e4dc Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 23:00:34 +0900 Subject: [PATCH 009/202] move ImagePainter, and fix mobile drawImage quality --- flutter/lib/desktop/pages/remote_page.dart | 38 +-------------------- flutter/lib/mobile/pages/remote_page.dart | 27 +-------------- flutter/lib/utils/image.dart | 39 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 63 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index a7289335f..211d36c39 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -21,6 +21,7 @@ import '../../mobile/widgets/dialog.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; +import '../../utils/image.dart'; import '../widgets/remote_menubar.dart'; import '../widgets/kb_layout_type_chooser.dart'; @@ -685,40 +686,3 @@ class CursorPaint extends StatelessWidget { ); } } - -class ImagePainter extends CustomPainter { - ImagePainter({ - required this.image, - required this.x, - required this.y, - required this.scale, - }); - - ui.Image? image; - double x; - double y; - double scale; - - @override - void paint(Canvas canvas, Size size) { - if (image == null) return; - if (x.isNaN || y.isNaN) return; - canvas.scale(scale, scale); - // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 - // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html - var paint = Paint(); - if ((scale - 1.0).abs() > 0.001) { - paint.filterQuality = FilterQuality.medium; - if (scale > 10.00000) { - paint.filterQuality = FilterQuality.high; - } - } - canvas.drawImage( - image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return oldDelegate != this; - } -} diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 853f3168c..956b985a7 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -17,6 +17,7 @@ import '../../common/widgets/remote_input.dart'; import '../../models/input_model.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +import '../../utils/image.dart'; import '../widgets/dialog.dart'; import '../widgets/gestures.dart'; @@ -898,32 +899,6 @@ class CursorPaint extends StatelessWidget { } } -class ImagePainter extends CustomPainter { - ImagePainter({ - required this.image, - required this.x, - required this.y, - required this.scale, - }); - - ui.Image? image; - double x; - double y; - double scale; - - @override - void paint(Canvas canvas, Size size) { - if (image == null) return; - canvas.scale(scale, scale); - canvas.drawImage(image!, Offset(x, y), Paint()); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return oldDelegate != this; - } -} - void showOptions( BuildContext context, String id, OverlayDialogManager dialogManager) async { String quality = diff --git a/flutter/lib/utils/image.dart b/flutter/lib/utils/image.dart index 1f0d5b0cd..7a6bcbc15 100644 --- a/flutter/lib/utils/image.dart +++ b/flutter/lib/utils/image.dart @@ -1,6 +1,8 @@ import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:flutter/widgets.dart'; + Future decodeImageFromPixels( Uint8List pixels, int width, @@ -47,3 +49,40 @@ Future decodeImageFromPixels( descriptor.dispose(); return frameInfo.image; } + +class ImagePainter extends CustomPainter { + ImagePainter({ + required this.image, + required this.x, + required this.y, + required this.scale, + }); + + ui.Image? image; + double x; + double y; + double scale; + + @override + void paint(Canvas canvas, Size size) { + if (image == null) return; + if (x.isNaN || y.isNaN) return; + canvas.scale(scale, scale); + // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 + // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html + var paint = Paint(); + if ((scale - 1.0).abs() > 0.001) { + paint.filterQuality = FilterQuality.medium; + if (scale > 10.00000) { + paint.filterQuality = FilterQuality.high; + } + } + canvas.drawImage( + image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return oldDelegate != this; + } +} From 58f67481344524fafa2e041e7214744efe16c7b5 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 23:14:24 +0900 Subject: [PATCH 010/202] fix physical keyboard on mobile does not work --- flutter/lib/common/widgets/remote_input.dart | 11 ++++- flutter/lib/mobile/pages/remote_page.dart | 52 ++++++++++---------- flutter/lib/models/input_model.dart | 14 +++--- 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 2fb409970..5833e760d 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/models/state_model.dart'; +import '../../common.dart'; import '../../models/input_model.dart'; class RawKeyFocusScope extends StatelessWidget { @@ -19,6 +20,13 @@ class RawKeyFocusScope extends StatelessWidget { @override Widget build(BuildContext context) { + final FocusOnKeyCallback? onKey; + if (isAndroid) { + onKey = inputModel.handleRawKeyEvent; + } else { + onKey = stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null; + } + return FocusScope( autofocus: true, child: Focus( @@ -26,8 +34,7 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKey: - stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null, + onKey: onKey, child: child)); } } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 956b985a7..9ae856250 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -581,9 +581,10 @@ class _RemotePageState extends State { child: Text(translate('Reset canvas')), value: 'reset_canvas')); } if (perms['keyboard'] != false) { - more.add(PopupMenuItem( - child: Text(translate('Physical Keyboard Input Mode')), - value: 'input-mode')); + // * Currently mobile does not enable map mode + // more.add(PopupMenuItem( + // child: Text(translate('Physical Keyboard Input Mode')), + // value: 'input-mode')); if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { more.add(PopupMenuItem( child: Text('${translate('Insert')} Ctrl + Alt + Del'), @@ -638,8 +639,9 @@ class _RemotePageState extends State { ); if (value == 'cad') { bind.sessionCtrlAltDel(id: widget.id); - } else if (value == 'input-mode') { - changePhysicalKeyboardInputMode(); + // * Currently mobile does not enable map mode + // } else if (value == 'input-mode') { + // changePhysicalKeyboardInputMode(); } else if (value == 'lock') { bind.sessionLockScreen(id: widget.id); } else if (value == 'block-input') { @@ -701,26 +703,26 @@ class _RemotePageState extends State { })); } - void changePhysicalKeyboardInputMode() async { - var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; - gFFI.dialogManager.show((setState, close) { - void setMode(String? v) async { - await bind.sessionPeerOption( - id: widget.id, name: "keyboard-mode", value: v ?? ""); - setState(() => current = v ?? ''); - Future.delayed(Duration(milliseconds: 300), close); - } - - return CustomAlertDialog( - title: Text(translate('Physical Keyboard Input Mode')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - getRadio('Legacy mode', 'legacy', current, setMode, - contentPadding: EdgeInsets.zero), - getRadio('Map mode', 'map', current, setMode, - contentPadding: EdgeInsets.zero), - ])); - }, clickMaskDismiss: true); - } + // * Currently mobile does not enable map mode + // void changePhysicalKeyboardInputMode() async { + // var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; + // gFFI.dialogManager.show((setState, close) { + // void setMode(String? v) async { + // await bind.sessionSetKeyboardMode(id: widget.id, value: v ?? ""); + // setState(() => current = v ?? ''); + // Future.delayed(Duration(milliseconds: 300), close); + // } + // + // return CustomAlertDialog( + // title: Text(translate('Physical Keyboard Input Mode')), + // content: Column(mainAxisSize: MainAxisSize.min, children: [ + // getRadio('Legacy mode', 'legacy', current, setMode, + // contentPadding: EdgeInsets.zero), + // getRadio('Map mode', 'map', current, setMode, + // contentPadding: EdgeInsets.zero), + // ])); + // }, clickMaskDismiss: true); + // } Widget getHelpTools() { final keyboard = isKeyboardShown(); diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 8c37f50bd..c37d01860 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -58,9 +58,12 @@ class InputModel { InputModel(this.parent); KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { - bind.sessionGetKeyboardMode(id: id).then((result) { - keyboardMode = result.toString(); - }); + // * Currently mobile does not enable map mode + if (isDesktop) { + bind.sessionGetKeyboardMode(id: id).then((result) { + keyboardMode = result.toString(); + }); + } final key = e.logicalKey; if (e is RawKeyDownEvent) { @@ -93,10 +96,9 @@ class InputModel { } } - if (keyboardMode == 'map') { + // * Currently mobile does not enable map mode + if (isDesktop && keyboardMode == 'map') { mapKeyboardMode(e); - } else if (keyboardMode == 'translate') { - legacyKeyboardMode(e); } else { legacyKeyboardMode(e); } From 628fa513f7402550c23ac96e63bd17958fc1f6d5 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 23:36:24 +0900 Subject: [PATCH 011/202] mobile remote_page.dart HelpTools add 'Insert' --- flutter/lib/mobile/pages/remote_page.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 9ae856250..54b6f1d47 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -814,6 +814,9 @@ class _RemotePageState extends State { wrap('End', () { inputModel.inputKey('VK_END'); }), + wrap('Ins', () { + inputModel.inputKey('VK_INSERT'); + }), wrap('Del', () { inputModel.inputKey('VK_DELETE'); }), From 73a2f41794a81603f8b603ac3a3c92e3f39fbe57 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Thu, 9 Feb 2023 16:18:36 +0100 Subject: [PATCH 012/202] Update es.rs New terms added --- src/lang/es.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 220447454..939a4831f 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", "Otras opciones predeterminadas"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Llamada de voz"), + ("Text chat", "Chat de texto"), + ("Stop voice call", "Detener llamada de voz"), ].iter().cloned().collect(); } From 37a3185c1c92c7fc69c016ada8d24f5dda8eea10 Mon Sep 17 00:00:00 2001 From: solokot Date: Thu, 9 Feb 2023 20:17:34 +0300 Subject: [PATCH 013/202] Update ru.rs --- src/lang/ru.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 1e6c6962a..1792eccce 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -415,7 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), ("Always use software rendering", "Использовать программную визуализацию"), ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), - ("config_microphone", ""), + ("config_microphone", "Чтобы разговаривать с удалённой стороной, необходимо предоставить RustDesk разрешение \"Запись аудио\"."), ("request_elevation_tip", "Также можно запросить повышение прав, если кто-то есть на удалённой стороне."), ("Wait", "Ждите"), ("Elevation Error", "Ошибка повышения прав"), @@ -435,19 +435,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Medium", "Средний"), ("Strong", "Стойкий"), ("Switch Sides", "Переключить стороны"), - ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), - ("Closed as expected", ""), + ("Please confirm if you want to share your desktop?", "Подтверждаете, что хотите поделиться своим рабочим столом?"), + ("Closed as expected", "Закрыто по ожиданию"), ("Display", "Отображение"), ("Default View Style", "Стиль отображения по умолчанию"), ("Default Scroll Style", "Стиль прокрутки по умолчанию"), ("Default Image Quality", "Качество изображения по умолчанию"), ("Default Codec", "Кодек по умолчанию"), ("Bitrate", "Битрейт"), - ("FPS", "FPS"), + ("FPS", "Частота кадров"), ("Auto", "Авто"), ("Other Default Options", "Другие параметры по умолчанию"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Голосовой вызов"), + ("Text chat", "Текстовый чат"), + ("Stop voice call", "Завершить голосовой вызов"), ].iter().cloned().collect(); } From 9d88a06cdfffde6d28612799420479e2177a8bfa Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 10 Feb 2023 15:05:35 +0800 Subject: [PATCH 014/202] showTitle default to false, change titlebar logo --- flutter/assets/logo.ico | Bin 270398 -> 0 bytes flutter/assets/logo.png | Bin 8643 -> 0 bytes flutter/assets/logo.svg | 2 +- .../lib/desktop/widgets/tabbar_widget.dart | 2 +- .../lib/desktop/widgets/titlebar_widget.dart | 41 +----------------- res/logo.svg | 2 +- 6 files changed, 4 insertions(+), 43 deletions(-) delete mode 100644 flutter/assets/logo.ico delete mode 100644 flutter/assets/logo.png diff --git a/flutter/assets/logo.ico b/flutter/assets/logo.ico deleted file mode 100644 index d5080c1f778ffb5ee61fc8429f558bbc7050aade..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 270398 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 LFD#*2eDVJS=SPuN diff --git a/flutter/assets/logo.png b/flutter/assets/logo.png deleted file mode 100644 index ede0e00c4447d6f08e3013ebc3d69365df0b0688..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8643 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 diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg index 0001d0762..965218c95 100644 --- a/flutter/assets/logo.svg +++ b/flutter/assets/logo.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 5c37900f2..9ba7a6315 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -234,7 +234,7 @@ class DesktopTab extends StatelessWidget { Key? key, required this.controller, this.showLogo = true, - this.showTitle = true, + this.showTitle = false, this.showMinimize = true, this.showMaximize = true, this.showClose = true, diff --git a/flutter/lib/desktop/widgets/titlebar_widget.dart b/flutter/lib/desktop/widgets/titlebar_widget.dart index 475b4cb86..38e4d917b 100644 --- a/flutter/lib/desktop/widgets/titlebar_widget.dart +++ b/flutter/lib/desktop/widgets/titlebar_widget.dart @@ -24,47 +24,8 @@ class DesktopTitleBar extends StatelessWidget { Expanded( child: child ?? Offstage(), ) - // const WindowButtons() ], ), ); } -} - -// final buttonColors = WindowButtonColors( -// iconNormal: const Color(0xFF805306), -// mouseOver: const Color(0xFFF6A00C), -// mouseDown: const Color(0xFF805306), -// iconMouseOver: const Color(0xFF805306), -// iconMouseDown: const Color(0xFFFFD500)); -// -// final closeButtonColors = WindowButtonColors( -// mouseOver: const Color(0xFFD32F2F), -// mouseDown: const Color(0xFFB71C1C), -// iconNormal: const Color(0xFF805306), -// iconMouseOver: Colors.white); -// -// class WindowButtons extends StatelessWidget { -// const WindowButtons({Key? key}) : super(key: key); -// -// @override -// Widget build(BuildContext context) { -// return Row( -// children: [ -// MinimizeWindowButton(colors: buttonColors, onPressed: () { -// windowManager.minimize(); -// },), -// MaximizeWindowButton(colors: buttonColors, onPressed: () async { -// if (await windowManager.isMaximized()) { -// windowManager.restore(); -// } else { -// windowManager.maximize(); -// } -// },), -// CloseWindowButton(colors: closeButtonColors, onPressed: () { -// windowManager.close(); -// },), -// ], -// ); -// } -// } +} \ No newline at end of file diff --git a/res/logo.svg b/res/logo.svg index 0001d0762..965218c95 100644 --- a/res/logo.svg +++ b/res/logo.svg @@ -1 +1 @@ - + \ No newline at end of file From 3c9e70d3a42b6038abf39050e4db2feefbe8ac5f Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 10 Feb 2023 09:31:43 +0100 Subject: [PATCH 015/202] fix autofocus --- flutter/lib/desktop/pages/desktop_setting_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 4b6cf2a62..366fb2ed7 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1861,7 +1861,7 @@ void changeSocks5Proxy() async { border: const OutlineInputBorder(), errorText: proxyMsg.isNotEmpty ? proxyMsg : null), controller: proxyController, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ), ], From be09728bf584030c1e79457bfd0e311b45548bee Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 10 Feb 2023 17:09:31 +0800 Subject: [PATCH 016/202] exclude ui module (sciter) for flutter --- src/cli.rs | 2 +- src/client/file_trait.rs | 4 +- src/common.rs | 11 +++ src/flutter_ffi.rs | 9 +-- src/lib.rs | 2 +- src/main.rs | 13 +++- src/ui.rs | 109 ++++++++++++++++++++++---- src/ui/macos.rs | 13 +--- src/ui_interface.rs | 161 +-------------------------------------- 9 files changed, 123 insertions(+), 201 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 117486ee4..40ab21188 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -36,7 +36,7 @@ impl Session { .lc .write() .unwrap() - .initialize(id.to_owned(), ConnType::PORT_FORWARD); + .initialize(id.to_owned(), ConnType::PORT_FORWARD, None); session } } diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 2ecfca837..49e3f2358 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -7,7 +7,7 @@ pub trait FileManager: Interface { fs::get_home_as_string() } - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter")))] fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value { match fs::read_dir(&fs::get_path(&path), include_hidden) { Err(_) => sciter::Value::null(), @@ -20,7 +20,7 @@ pub trait FileManager: Interface { } } - #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] + #[cfg(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter"))] fn read_dir(&self, path: &str, include_hidden: bool) -> String { use crate::common::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden) { diff --git a/src/common.rs b/src/common.rs index 79a4664db..b66261ebe 100644 --- a/src/common.rs +++ b/src/common.rs @@ -762,3 +762,14 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin fd_json.insert("entries".into(), json!(entries_out)); serde_json::to_string(&fd_json).unwrap_or("".into()) } + +/// The function to handle the url scheme sent by the system. +/// +/// 1. Try to send the url scheme from ipc. +/// 2. If failed to send the url scheme, we open a new main window to handle this url scheme. +pub fn handle_url_scheme(url: String) { + if let Err(err) = crate::ipc::send_url_scheme(url.clone()) { + log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); + let _ = crate::run_me(vec![url]); + } +} \ No newline at end of file diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a7e32d0b2..a79ef2de8 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1119,13 +1119,6 @@ pub fn cm_switch_back(conn_id: i32) { crate::ui_cm_interface::switch_back(conn_id); } -pub fn main_get_icon() -> String { - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] - return ui_interface::get_icon(); - #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] - return String::new(); -} - pub fn main_get_build_date() -> String { crate::BUILD_DATE.to_string() } @@ -1305,7 +1298,7 @@ pub fn main_start_ipc_url_server() { #[allow(unused_variables)] pub fn send_url_scheme(_url: String) { #[cfg(target_os = "macos")] - std::thread::spawn(move || crate::ui::macos::handle_url_scheme(_url)); + std::thread::spawn(move || crate::handle_url_scheme(_url)); } #[cfg(target_os = "android")] diff --git a/src/lib.rs b/src/lib.rs index 7b94c8a2c..748d375b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ pub use self::rendezvous_mediator::*; pub mod common; #[cfg(not(any(target_os = "ios")))] pub mod ipc; -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter")))] pub mod ui; mod version; pub use version::*; diff --git a/src/main.rs b/src/main.rs index 6500a8e4a..8bc375841 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ // Requires Rust 1.18. //#![windows_subsystem = "windows"] +#[cfg(not(feature = "flutter"))] use librustdesk::*; #[cfg(any(target_os = "android", target_os = "ios"))] @@ -16,7 +17,12 @@ fn main() { common::global_clean(); } -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +#[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" +)))] fn main() { if !common::global_init() { return; @@ -27,6 +33,11 @@ fn main() { common::global_clean(); } +#[cfg(feature = "flutter")] +fn main() { + hbb_common::log::info!("Hello world!"); +} + #[cfg(feature = "cli")] fn main() { if !common::global_init() { diff --git a/src/ui.rs b/src/ui.rs index 7973a0ba4..aede5fe7a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,7 +9,7 @@ use sciter::Value; use hbb_common::{ allow_err, - config::{self, PeerConfig}, + config::{self, LocalConfig, PeerConfig}, log, }; @@ -38,6 +38,7 @@ lazy_static::lazy_static! { #[cfg(not(any(feature = "flutter", feature = "cli")))] lazy_static::lazy_static! { pub static ref CUR_SESSION: Arc>>> = Default::default(); + static ref CHILDREN : Children = Default::default(); } struct UIHostHandler; @@ -190,11 +191,11 @@ impl UI { } fn get_remote_id(&mut self) -> String { - get_remote_id() + LocalConfig::get_remote_id() } fn set_remote_id(&mut self, id: String) { - set_remote_id(id); + LocalConfig::set_remote_id(&id); } fn goto_install(&mut self) { @@ -309,7 +310,10 @@ impl UI { } fn is_release(&self) -> bool { - is_release() + #[cfg(not(debug_assertions))] + return true; + #[cfg(debug_assertions)] + return false; } fn is_rdp_service_open(&self) -> bool { @@ -329,11 +333,18 @@ impl UI { } fn closing(&mut self, x: i32, y: i32, w: i32, h: i32) { - closing(x, y, w, h) + crate::server::input_service::fix_key_down_timeout_at_exit(); + LocalConfig::set_size(x, y, w, h); } fn get_size(&mut self) -> Value { - Value::from_iter(get_size()) + let s = LocalConfig::get_size(); + let mut v = Vec::new(); + v.push(s.0); + v.push(s.1); + v.push(s.2); + v.push(s.3); + Value::from_iter(v) } fn get_mouse_time(&self) -> f64 { @@ -388,7 +399,7 @@ impl UI { fn get_recent_sessions(&mut self) -> Value { // to-do: limit number of recent sessions, and remove old peer file - let peers: Vec = get_recent_sessions() + let peers: Vec = PeerConfig::peers() .drain(..) .map(|p| Self::get_peer_value(p.0, p.2)) .collect(); @@ -396,11 +407,11 @@ impl UI { } fn get_icon(&mut self) -> String { - get_icon() + crate::get_icon() } fn remove_peer(&mut self, id: String) { - remove_peer(id) + PeerConfig::remove(&id); } fn remove_discovered(&mut self, id: String) { @@ -442,7 +453,7 @@ impl UI { } fn get_software_update_url(&self) -> String { - get_software_update_url() + crate::SOFTWARE_UPDATE_URL.lock().unwrap().clone() } fn get_new_version(&self) -> String { @@ -458,14 +469,30 @@ impl UI { } fn get_software_ext(&self) -> String { - get_software_ext() + #[cfg(windows)] + let p = "exe"; + #[cfg(target_os = "macos")] + let p = "dmg"; + #[cfg(target_os = "linux")] + let p = "deb"; + p.to_owned() } fn get_software_store_path(&self) -> String { - get_software_store_path() + let mut p = std::env::temp_dir(); + let name = crate::SOFTWARE_UPDATE_URL + .lock() + .unwrap() + .split("/") + .last() + .map(|x| x.to_owned()) + .unwrap_or(crate::get_app_name()); + p.push(name); + format!("{}.{}", p.to_string_lossy(), self.get_software_ext()) } fn create_shortcut(&self, _id: String) { + #[cfg(windows)] create_shortcut(_id) } @@ -495,7 +522,17 @@ impl UI { } fn open_url(&self, url: String) { - open_url(url) + #[cfg(windows)] + let p = "explorer"; + #[cfg(target_os = "macos")] + let p = "open"; + #[cfg(target_os = "linux")] + let p = if std::path::Path::new("/usr/bin/firefox").exists() { + "firefox" + } else { + "xdg-open" + }; + allow_err!(std::process::Command::new(p).arg(url).spawn()); } fn change_id(&self, id: String) { @@ -508,7 +545,7 @@ impl UI { } fn is_ok_change_id(&self) -> bool { - is_ok_change_id() + machine_uid::get().is_ok() } fn get_async_job_status(&self) -> String { @@ -516,11 +553,11 @@ impl UI { } fn t(&self, name: String) -> String { - t(name) + crate::client::translate(name) } fn is_xfce(&self) -> bool { - is_xfce() + crate::platform::is_xfce() } fn get_api_server(&self) -> String { @@ -683,3 +720,43 @@ pub fn value_crash_workaround(values: &[Value]) -> Arc> { STUPID_VALUES.lock().unwrap().push(persist.clone()); persist } + +#[inline] +pub fn new_remote(id: String, remote_type: String) { + let mut lock = CHILDREN.lock().unwrap(); + let args = vec![format!("--{}", remote_type), id.clone()]; + let key = (id.clone(), remote_type.clone()); + if let Some(c) = lock.1.get_mut(&key) { + if let Ok(Some(_)) = c.try_wait() { + lock.1.remove(&key); + } else { + if remote_type == "rdp" { + allow_err!(c.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + c.try_wait().ok(); + lock.1.remove(&key); + } else { + return; + } + } + } + match crate::run_me(args) { + Ok(child) => { + lock.1.insert(key, child); + } + Err(err) => { + log::error!("Failed to spawn remote: {}", err); + } + } +} + +#[inline] +pub fn recent_sessions_updated() -> bool { + let mut children = CHILDREN.lock().unwrap(); + if children.0 { + children.0 = false; + true + } else { + false + } +} diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 8a1fc990c..cd0e5871b 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -180,22 +180,11 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { } } -/// The function to handle the url scheme sent by the system. -/// -/// 1. Try to send the url scheme from ipc. -/// 2. If failed to send the url scheme, we open a new main window to handle this url scheme. -pub fn handle_url_scheme(url: String) { - if let Err(err) = crate::ipc::send_url_scheme(url.clone()) { - log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); - let _ = crate::run_me(vec![url]); - } -} - extern "C" fn handle_apple_event(_this: &Object, _cmd: Sel, event: u64, _reply: u64) { let event = event as *mut Object; let url = fruitbasket::parse_url_event(event); log::debug!("an event was received: {}", url); - std::thread::spawn(move || handle_url_scheme(url)); + std::thread::spawn(move || crate::handle_url_scheme(url)); } unsafe fn make_menu_item(title: &str, key: &str, tag: u32) -> *mut Object { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index d357c9cef..6576c340c 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -2,7 +2,6 @@ use std::{ collections::HashMap, process::Child, sync::{Arc, Mutex}, - time::SystemTime, }; #[cfg(any(target_os = "android", target_os = "ios"))] @@ -31,7 +30,6 @@ pub type Children = Arc)>>; type Status = (i32, bool, i64, String); // (status_num, key_confirmed, mouse_time, id) lazy_static::lazy_static! { - static ref CHILDREN : Children = Default::default(); static ref UI_STATUS : Arc> = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); static ref OPTIONS : Arc>> = Arc::new(Mutex::new(Config::get_options())); static ref ASYNC_JOB_STATUS : Arc> = Default::default(); @@ -44,17 +42,6 @@ lazy_static::lazy_static! { pub static ref SENDER : Mutex> = Mutex::new(check_connect_status(true)); } -#[inline] -pub fn recent_sessions_updated() -> bool { - let mut children = CHILDREN.lock().unwrap(); - if children.0 { - children.0 = false; - true - } else { - false - } -} - #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn get_id() -> String { @@ -64,16 +51,6 @@ pub fn get_id() -> String { return ipc::get_id(); } -#[inline] -pub fn get_remote_id() -> String { - LocalConfig::get_remote_id() -} - -#[inline] -pub fn set_remote_id(id: String) { - LocalConfig::set_remote_id(&id); -} - #[inline] pub fn goto_install() { allow_err!(crate::run_me(vec!["--install"])); @@ -419,24 +396,6 @@ pub fn is_installed_lower_version() -> bool { } } -#[inline] -pub fn closing(x: i32, y: i32, w: i32, h: i32) { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - crate::server::input_service::fix_key_down_timeout_at_exit(); - LocalConfig::set_size(x, y, w, h); -} - -#[inline] -pub fn get_size() -> Vec { - let s = LocalConfig::get_size(); - let mut v = Vec::new(); - v.push(s.0); - v.push(s.1); - v.push(s.2); - v.push(s.3); - v -} - #[inline] pub fn get_mouse_time() -> f64 { let ui_status = UI_STATUS.lock().unwrap(); @@ -507,51 +466,6 @@ pub fn store_fav(fav: Vec) { LocalConfig::set_fav(fav); } -#[inline] -pub fn get_recent_sessions() -> Vec<(String, SystemTime, PeerConfig)> { - PeerConfig::peers() -} - -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn get_icon() -> String { - crate::get_icon() -} - -#[inline] -pub fn remove_peer(id: String) { - PeerConfig::remove(&id); -} - -#[inline] -pub fn new_remote(id: String, remote_type: String) { - let mut lock = CHILDREN.lock().unwrap(); - let args = vec![format!("--{}", remote_type), id.clone()]; - let key = (id.clone(), remote_type.clone()); - if let Some(c) = lock.1.get_mut(&key) { - if let Ok(Some(_)) = c.try_wait() { - lock.1.remove(&key); - } else { - if remote_type == "rdp" { - allow_err!(c.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); - c.try_wait().ok(); - lock.1.remove(&key); - } else { - return; - } - } - } - match crate::run_me(args) { - Ok(child) => { - lock.1.insert(key, child); - } - Err(err) => { - log::error!("Failed to spawn remote: {}", err); - } - } -} - #[inline] pub fn is_process_trusted(_prompt: bool) -> bool { #[cfg(target_os = "macos")] @@ -622,11 +536,6 @@ pub fn current_is_wayland() -> bool { return false; } -#[inline] -pub fn get_software_update_url() -> String { - SOFTWARE_UPDATE_URL.lock().unwrap().clone() -} - #[inline] pub fn get_new_version() -> String { hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) @@ -643,36 +552,9 @@ pub fn get_app_name() -> String { crate::get_app_name() } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn get_software_ext() -> String { - #[cfg(windows)] - let p = "exe"; - #[cfg(target_os = "macos")] - let p = "dmg"; - #[cfg(target_os = "linux")] - let p = "deb"; - p.to_owned() -} - -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn get_software_store_path() -> String { - let mut p = std::env::temp_dir(); - let name = SOFTWARE_UPDATE_URL - .lock() - .unwrap() - .split("/") - .last() - .map(|x| x.to_owned()) - .unwrap_or(crate::get_app_name()); - p.push(name); - format!("{}.{}", p.to_string_lossy(), get_software_ext()) -} - +#[cfg(windows)] #[inline] pub fn create_shortcut(_id: String) { - #[cfg(windows)] crate::platform::windows::create_shortcut(&_id).ok(); } @@ -719,22 +601,6 @@ pub fn get_uuid() -> String { base64::encode(hbb_common::get_uuid()) } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn open_url(url: String) { - #[cfg(windows)] - let p = "explorer"; - #[cfg(target_os = "macos")] - let p = "open"; - #[cfg(target_os = "linux")] - let p = if std::path::Path::new("/usr/bin/firefox").exists() { - "firefox" - } else { - "xdg-open" - }; - allow_err!(std::process::Command::new(p).arg(url).spawn()); -} - #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn change_id(id: String) { @@ -756,23 +622,11 @@ pub fn post_request(url: String, body: String, header: String) { }); } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn is_ok_change_id() -> bool { - machine_uid::get().is_ok() -} - #[inline] pub fn get_async_job_status() -> String { ASYNC_JOB_STATUS.lock().unwrap().clone() } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn t(name: String) -> String { - crate::client::translate(name) -} - #[inline] pub fn get_langs() -> String { crate::lang::LANGS.to_string() @@ -813,11 +667,6 @@ pub fn default_video_save_directory() -> String { "".to_owned() } -#[inline] -pub fn is_xfce() -> bool { - crate::platform::is_xfce() -} - #[inline] pub fn get_api_server() -> String { crate::get_api_server( @@ -834,14 +683,6 @@ pub fn has_hwcodec() -> bool { return true; } -#[inline] -pub fn is_release() -> bool { - #[cfg(not(debug_assertions))] - return true; - #[cfg(debug_assertions)] - return false; -} - #[cfg(not(any(target_os = "android", target_os = "ios")))] #[inline] pub fn is_root() -> bool { From 930faecb13fbf3761f66aeeea7371903b5e741f3 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 10 Feb 2023 17:38:08 +0800 Subject: [PATCH 017/202] fix ci --- src/ui_interface.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 6576c340c..26038218e 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -511,9 +511,9 @@ pub fn get_error() -> String { if dtype != "x11" { return format!( "{} {}, {}", - t("Unsupported display server ".to_owned()), + crate::client::translate("Unsupported display server ".to_owned()), dtype, - t("x11 expected".to_owned()), + crate::client::translate("x11 expected".to_owned()), ); } } From 7edb3e6e92a90ba520edc52d8b66354c0f9a0378 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 10 Feb 2023 17:48:53 +0800 Subject: [PATCH 018/202] CI --- src/lib.rs | 2 ++ src/main.rs | 8 +------- src/platform/windows.rs | 12 ++++++------ src/server/connection.rs | 4 ++-- src/server/video_service.rs | 10 +++++----- src/ui.rs | 2 -- src/ui_cm_interface.rs | 2 +- src/{ui => }/win_privacy.rs | 0 8 files changed, 17 insertions(+), 23 deletions(-) rename src/{ui => }/win_privacy.rs (100%) diff --git a/src/lib.rs b/src/lib.rs index 748d375b4..5dcd6389c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,3 +56,5 @@ pub mod clipboard_file; #[cfg(all(windows, feature = "with_rc"))] pub mod rc; +#[cfg(target_os = "windows")] +pub mod win_privacy; diff --git a/src/main.rs b/src/main.rs index 8bc375841..169515425 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,9 @@ // Requires Rust 1.18. //#![windows_subsystem = "windows"] -#[cfg(not(feature = "flutter"))] use librustdesk::*; -#[cfg(any(target_os = "android", target_os = "ios"))] +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] fn main() { if !common::global_init() { return; @@ -33,11 +32,6 @@ fn main() { common::global_clean(); } -#[cfg(feature = "flutter")] -fn main() { - hbb_common::log::info!("Hello world!"); -} - #[cfg(feature = "cli")] fn main() { if !common::global_init() { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 17f275c2a..bd6a1fc4c 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -833,8 +833,8 @@ fn get_default_install_path() -> String { pub fn check_update_broker_process() -> ResultType<()> { // let (_, path, _, _) = get_install_info(); - let process_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE; - let origin_process_exe = crate::ui::win_privacy::ORIGIN_PROCESS_EXE; + let process_exe = crate::win_privacy::INJECTED_PROCESS_EXE; + let origin_process_exe = crate::win_privacy::ORIGIN_PROCESS_EXE; let exe_file = std::env::current_exe()?; if exe_file.parent().is_none() { @@ -919,8 +919,8 @@ pub fn copy_exe_cmd(src_exe: &str, _exe: &str, path: &str) -> String { ", main_exe = main_exe, path = path, - ORIGIN_PROCESS_EXE = crate::ui::win_privacy::ORIGIN_PROCESS_EXE, - broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, + ORIGIN_PROCESS_EXE = crate::win_privacy::ORIGIN_PROCESS_EXE, + broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, ); } @@ -938,7 +938,7 @@ pub fn update_me() -> ResultType<()> { {lic} ", copy_exe = copy_exe_cmd(&src_exe, &exe, &path), - broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, + broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, app_name = crate::get_app_name(), lic = register_licence(), cur_pid = get_current_pid(), @@ -1203,7 +1203,7 @@ fn get_before_uninstall() -> String { netsh advfirewall firewall delete rule name=\"{app_name} Service\" ", app_name = app_name, - broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, + broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, ext = ext, cur_pid = get_current_pid(), ) diff --git a/src/server/connection.rs b/src/server/connection.rs index 9ce53c960..53ccd7008 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2045,7 +2045,7 @@ mod privacy_mode { pub(super) fn turn_off_privacy(_conn_id: i32) -> Message { #[cfg(windows)] { - use crate::ui::win_privacy::*; + use crate::win_privacy::*; let res = turn_off_privacy(_conn_id, None); match res { @@ -2069,7 +2069,7 @@ mod privacy_mode { pub(super) fn turn_on_privacy(_conn_id: i32) -> ResultType { #[cfg(windows)] { - let plugin_exist = crate::ui::win_privacy::turn_on_privacy(_conn_id)?; + let plugin_exist = crate::win_privacy::turn_on_privacy(_conn_id)?; Ok(plugin_exist) } #[cfg(not(windows))] diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 57fdf2c22..bc9c5ff6f 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -207,7 +207,7 @@ fn create_capturer( if privacy_mode_id > 0 { #[cfg(windows)] { - use crate::ui::win_privacy::*; + use crate::win_privacy::*; match scrap::CapturerMag::new( display.origin(), @@ -308,11 +308,11 @@ pub fn test_create_capturer(privacy_mode_id: i32, timeout_millis: u64) -> bool { fn check_uac_switch(privacy_mode_id: i32, capturer_privacy_mode_id: i32) -> ResultType<()> { if capturer_privacy_mode_id != 0 { if privacy_mode_id != capturer_privacy_mode_id { - if !crate::ui::win_privacy::is_process_consent_running()? { + if !crate::win_privacy::is_process_consent_running()? { bail!("consent.exe is running"); } } - if crate::ui::win_privacy::is_process_consent_running()? { + if crate::win_privacy::is_process_consent_running()? { bail!("consent.exe is running"); } } @@ -372,7 +372,7 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType)>>; #[allow(dead_code)] diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index de33b0169..f5c575d43 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -494,7 +494,7 @@ pub async fn start_ipc(cm: ConnectionManager) { e ); } - allow_err!(crate::ui::win_privacy::start()); + allow_err!(crate::win_privacy::start()); }); match ipc::new_listener("_cm").await { diff --git a/src/ui/win_privacy.rs b/src/win_privacy.rs similarity index 100% rename from src/ui/win_privacy.rs rename to src/win_privacy.rs From 23f133b83674347f8bd7f9e61f6c764e0dda23cc Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 10 Feb 2023 10:50:48 +0100 Subject: [PATCH 019/202] unify padding of dialogs --- flutter/lib/common.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a731f0b08..4ad4a9927 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -648,8 +648,6 @@ class CustomAlertDialog extends StatelessWidget { child: AlertDialog( scrollable: true, title: title, - contentPadding: EdgeInsets.fromLTRB( - contentPadding ?? padding, 25, contentPadding ?? padding, 10), content: ConstrainedBox( constraints: contentBoxConstraints, child: Theme( From 07b86bee8e521872048e159bdd213f09335b22a2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 10 Feb 2023 18:26:23 +0800 Subject: [PATCH 020/202] try fix memory issue when decoding is too slow Signed-off-by: fufesou --- flutter/lib/models/model.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index ca99a5bd1..feab5bdc8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -415,6 +415,8 @@ class ImageModel with ChangeNotifier { String id = ''; + int decodeCount = 0; + WeakReference parent; final List _callbacksOnFirstImage = []; @@ -434,7 +436,13 @@ class ImageModel with ChangeNotifier { } } } + + if (decodeCount >= 1) { + return; + } + final pid = parent.target?.id; + decodeCount += 1; ui.decodeImageFromPixels( rgba, parent.target?.ffiModel.display.width ?? 0, @@ -442,6 +450,7 @@ class ImageModel with ChangeNotifier { isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { if (parent.target?.id != pid) return; try { + decodeCount -= 1; // my throw exception, because the listener maybe already dispose update(image); } catch (e) { From 7ccee565095647c3553f1fb6e79c5f0ecf854cf7 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Fri, 10 Feb 2023 10:34:19 +0000 Subject: [PATCH 021/202] need not required for docker >23.0.1 --- .devcontainer/devcontainer.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 24ba9a915..cc348f38f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,10 +1,7 @@ { "name": "rustdesk", "build": { - "dockerfile": "Dockerfile", - "args": { - "BUILDKIT_INLINE_CACHE": "0" - } + "dockerfile": "Dockerfile" }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/user/rustdesk,type=bind,consistency=cache", "workspaceFolder": "/home/user/rustdesk", From a73514c35b9b7403b743628c1e5e3cb111217bee Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 10 Feb 2023 18:35:02 +0800 Subject: [PATCH 022/202] fix counter logic Signed-off-by: fufesou --- flutter/lib/models/model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index feab5bdc8..add1289e2 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -448,9 +448,9 @@ class ImageModel with ChangeNotifier { parent.target?.ffiModel.display.width ?? 0, parent.target?.ffiModel.display.height ?? 0, isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { + decodeCount -= 1; if (parent.target?.id != pid) return; try { - decodeCount -= 1; // my throw exception, because the listener maybe already dispose update(image); } catch (e) { From 5b36555faa97a48d26cfda2bc95c58e71ef91294 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 10 Feb 2023 18:42:08 +0800 Subject: [PATCH 023/202] flutter option enable share rdp Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 28 +++++++++++++++++++ src/flutter_ffi.rs | 4 +++ 2 files changed, 32 insertions(+) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 4b6cf2a62..5d524523a 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -701,6 +701,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp', enabled: enabled), ), + shareRdp(context, enabled), _OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery', reverse: true, enabled: enabled), ...directIp(context), @@ -708,6 +709,33 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ]); } + shareRdp(BuildContext context, bool enabled) { + onChanged(bool b) async { + await bind.mainSetShareRdp(enable: b); + setState(() {}); + } + + bool value = bind.mainIsShareRdp(); + return Offstage( + offstage: !(Platform.isWindows && bind.mainIsRdpServiceOpen()), + child: GestureDetector( + child: Row( + children: [ + Checkbox( + value: value, + onChanged: enabled ? (_) => onChanged(!value) : null) + .marginOnly(right: 5), + Expanded( + child: Text(translate('Enable RDP session sharing'), + style: + TextStyle(color: _disabledTextColor(context, enabled))), + ) + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: enabled ? () => onChanged(!value) : null), + ); + } + List directIp(BuildContext context) { TextEditingController controller = TextEditingController(); update() => setState(() {}); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a7e32d0b2..3611b5dbf 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1210,6 +1210,10 @@ pub fn main_is_rdp_service_open() -> SyncReturn { SyncReturn(is_rdp_service_open()) } +pub fn main_set_share_rdp(enable: bool) { + set_share_rdp(enable) +} + pub fn main_goto_install() -> SyncReturn { goto_install(); SyncReturn(true) From b4357e1e000f4914953385dd23982aceb776a863 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 10 Feb 2023 12:51:49 +0100 Subject: [PATCH 024/202] fix icon name --- flutter/assets/{Github.svg => GitHub.svg} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename flutter/assets/{Github.svg => GitHub.svg} (100%) diff --git a/flutter/assets/Github.svg b/flutter/assets/GitHub.svg similarity index 100% rename from flutter/assets/Github.svg rename to flutter/assets/GitHub.svg From 554b8bd0324a58ddf07a16caeb1f205dc933ee30 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 10 Feb 2023 14:14:49 +0100 Subject: [PATCH 025/202] Addressbook login. Button instead of text --- flutter/lib/common/widgets/address_book.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 5c1e1218c..5cd2af2be 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -43,13 +43,10 @@ class _AddressBookState extends State { return Obx(() { if (gFFI.userModel.userName.value.isEmpty) { return Center( - child: InkWell( - onTap: loginDialog, - child: Text( - translate("Login"), - style: const TextStyle(decoration: TextDecoration.underline), - ), - ), + child: ElevatedButton( + onPressed: loginDialog, + child: Text(translate("Login")) + ) ); } else { if (gFFI.abModel.abLoading.value) { From 19c7cd99d57f91b4697eed912961ac53f9410250 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 10 Feb 2023 21:18:55 +0800 Subject: [PATCH 026/202] fix: --cm cannot exit on macOS --- flutter/lib/common.dart | 5 +++++ flutter/lib/desktop/pages/server_page.dart | 15 +++++++++++++-- flutter/lib/models/model.dart | 2 +- flutter/lib/models/server_model.dart | 6 ++---- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 4ad4a9927..d86960a0d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -49,6 +49,11 @@ int androidVersion = 0; int windowsBuildNumber = 0; DesktopType? desktopType; +/// Check if the app is running with single view mode. +bool isSingleViewApp() { + return desktopType == DesktopType.cm; +} + /// * debug or test only, DO NOT enable in release build bool isTest = false; diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index b66a08e74..252e1cd12 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -1,11 +1,13 @@ // original cm window in Sciter version. import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/consts.dart'; 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:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; @@ -47,8 +49,17 @@ class _DesktopServerPageState extends State @override void onWindowClose() { - gFFI.serverModel.closeAll(); - gFFI.close(); + Future.wait([ + gFFI.serverModel.closeAll(), + gFFI.close() + ]).then((_) { + if (Platform.isMacOS) { + RdPlatformChannel.instance.terminate(); + } else { + windowManager.setPreventClose(false); + windowManager.close(); + } + }); super.onWindowClose(); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index add1289e2..eb837ba70 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1399,12 +1399,12 @@ class FFI { await setCanvasConfig(id, cursorModel.x, cursorModel.y, canvasModel.x, canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } - bind.sessionClose(id: id); imageModel.update(null); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); inputModel.resetModifiers(); + await bind.sessionClose(id: id); debugPrint('model $id closed'); id = ''; } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index aab12ab5d..b2043f3c2 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -560,10 +560,8 @@ class ServerModel with ChangeNotifier { } } - closeAll() { - for (var client in _clients) { - bind.cmCloseConnection(connId: client.id); - } + Future closeAll() async { + await Future.wait(_clients.map((client) => bind.cmCloseConnection(connId: client.id))); _clients.clear(); tabController.state.value.tabs.clear(); } From cfc6f4b88a5c362226e029df5f0c8cc9a78b638b Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 10 Feb 2023 21:32:51 +0800 Subject: [PATCH 027/202] mouse do not control in black blank area Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index c37d01860..b1491d526 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -485,10 +485,19 @@ class InputModel { y /= canvasModel.scale; x += d.x; y += d.y; + + if (x < d.x || y < d.y || x > (d.x + d.width) || y > (d.y + d.height)) { + // If left mouse up, no early return. + if (evt['buttons'] != kPrimaryMouseButton || type != 'up') { + return; + } + } + if (type != '') { x = 0; y = 0; } + evt['x'] = '${x.round()}'; evt['y'] = '${y.round()}'; var buttons = ''; From 3e17fd372b21a6cbfa7188a03d0a5ffd030c6e80 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Fri, 10 Feb 2023 23:33:52 +0800 Subject: [PATCH 028/202] Revert "unify padding of dialogs" --- flutter/lib/common.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index d86960a0d..a295ad4f8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -653,6 +653,8 @@ class CustomAlertDialog extends StatelessWidget { child: AlertDialog( scrollable: true, title: title, + contentPadding: EdgeInsets.fromLTRB( + contentPadding ?? padding, 25, contentPadding ?? padding, 10), content: ConstrainedBox( constraints: contentBoxConstraints, child: Theme( From d416d7d9658abfd5cd3ab954c9cb34d1a3e41b99 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 11 Feb 2023 00:21:19 +0800 Subject: [PATCH 029/202] base64 icon only for sciter --- libs/hbb_common/src/config.rs | 8 +------- src/common.rs | 7 +------ src/ui.rs | 15 ++++++++++++++- src/ui/cm.rs | 2 +- src/ui/remote.rs | 2 +- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 1e4d80c9f..3bfc885c5 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -30,13 +30,7 @@ pub const REG_INTERVAL: i64 = 12_000; pub const COMPRESS_LEVEL: i32 = 3; const SERIAL: i32 = 3; const PASSWORD_ENC_VERSION: &str = "00"; -// 128x128 -#[cfg(target_os = "macos")] // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding -pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAyVBMVEUAAAAAcf8Acf8Acf8Acv8Acf8Acf8Acf8Acf8AcP8Acf8Ab/8AcP8Acf////8AaP/z+f/o8v/k7v/5/v/T5f8AYP/u9v/X6f+hx/+Kuv95pP8Aef/B1/+TwP9xoP8BdP/g6P+Irv9ZmP8Bgf/E3f98q/9sn/+01f+Es/9nm/9Jif8hhv8off/M4P+syP+avP86iP/c7f+xy/9yqf9Om/9hk/9Rjv+60P99tv9fpf88lv8yjf8Tgf8deP+kvP8BiP8NeP8hkP80gP8oj2VLAAAADXRSTlMA7o7qLvnaxZ1FOxYPjH9HWgAABHJJREFUeNrtm+tW4jAQgBfwuu7MtIUWsOUiCCioIIgLiqvr+z/UHq/LJKVkmwTcc/r9E2nzlU4mSTP9lpGRkZGR8VX5cZjfL+yCEXYL+/nDH//U/Pd8DgyTy39Xbv7oIAcWyB0cqbW/sweW2NtRaj8H1sgpGOwUIAH7Bkd7YJW9dXFwAJY5WNP/cmCZQnJvzIN18on5LwfWySXlxEPYAIcad8D6PdiHDbCfIFCADVBIENiFDbCbIACKPPXrZ+cP8E6/0znvP4EymgIEravIRcTxu8HxNSJ60a8W0AYECKrlAN+YwAthCd9wm1Ug6wKzIn5SgRduXfwkqDasCjx0XFzi9PV6zwNcIuhcWBOg+ikySq8C9UD4dEKWBCoOcspvAuLHTo9sCDQiFPHotRM48j8G5gVur1FdAN2uaYEuiz7xFsgEJ2RUoMUakXuBTHHoGxQYOBhHjeUBAefEnMAowFhaLBOKuOemBBbxLRQrH2PBCgMvNCPQGMeevTb9zLrPxz2Mo+QbEaijzPUcOOHMQZkKGRAIPem39+bypREMPTkQW/oCfk866zAkiIFG4yIKRE/aAnfiSd0WrORY6pFdXQEqi9mvAQm0RIOSnoCcZ8vJoz3diCnjRk+g8VP4/fuQDJ2Lxr6WwG0gXs9aTpDzW0vgDBlVUpixR8gYk44AD8FrUKHr8JQJGgIDnoDqoALxmWPQSi9AVVzm8gKUuEPGr/QCvptwJkbSYT/TC4S8C96DGjTj86aHtAI0x2WaBIq0eSYYpRa4EsdWVVwWu9O0Aj6f6dyBMnwEraeOgSYu0wZlauzA47QCbT7DgAQSE+hZWoEBF/BBmWOewNMK3BsSqKUW4MGcWqCSVmDkbvkXGKQOwg6PAUO9oL3xXhA20yaiCjuwYygRVQlUOTWTCf2SuNJTxeFjgaHByGuAIvd8ItdPLTDhS7IuqEE1YSKVOgbayLhSFQhMzYh8hwfBs1r7c505YVIQYEdNoKwxK06MJiyrpUFHiF0NAfCQUVHoiRclIXJIR6C2fqG37pBHvcWpgwzvAtYwkR5UGV2e42UISdBJETl3mg8ouo54Rcnti1/vaT+iuUQBt500Cgo4U10BeHSkk57FB0JjWkKRMWgLUA0lLodtImAQdaMiiri3+gIAPZQoutHNsgKF1aaDMhMyIdBf8Th+Bh8MTjGWCpl5Wv43tDmnF+IUVMrcZgRoiAxhtrloYizNkZaAnF5leglbNhj0wYCAbCDvGb0mP4nib7O7ZlcYQ2m1gPtIZgVgGNNMeaVAaWR+57TrqgtUnm3sHQ+kYeE6fufUubG1ez50FXbPnWgBlgSABmN3TTcsRl2yWkHRrwbiunvk/W2+Mg1hPZplPDeXRbZzStFH15s1QIVd3UImP5z/bHpeeQLvRJ7XLFUffQIlCvqlXETQbgN9/rlYABGosv+Vi9m2Xs639YLGrZd0br+odetlvdsvbN56abfd4vbCzv9Q3v/ygoOV21A4OPpfXvH4Ai+5ZGRkZGRkbJA/t/I0QMzoMiEAAAAASUVORK5CYII= -"; -#[cfg(not(target_os = "macos"))] // 128x128 no padding -pub const ICON: &str = "data:image/png;base64,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 -"; + #[cfg(target_os = "macos")] lazy_static::lazy_static! { pub static ref ORG: Arc> = Arc::new(RwLock::new("com.carriez".to_owned())); diff --git a/src/common.rs b/src/common.rs index b66261ebe..ee44cf4f2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -588,11 +588,6 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { Ok(()) } -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn get_icon() -> String { - hbb_common::config::ICON.to_owned() -} - pub fn get_app_name() -> String { hbb_common::config::APP_NAME.read().unwrap().clone() } @@ -772,4 +767,4 @@ pub fn handle_url_scheme(url: String) { log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); let _ = crate::run_me(vec![url]); } -} \ No newline at end of file +} diff --git a/src/ui.rs b/src/ui.rs index ce97745fb..1b6838e46 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -405,7 +405,7 @@ impl UI { } fn get_icon(&mut self) -> String { - crate::get_icon() + get_icon() } fn remove_peer(&mut self, id: String) { @@ -758,3 +758,16 @@ pub fn recent_sessions_updated() -> bool { false } } + +pub fn get_icon() -> String { + // 128x128 + #[cfg(target_os = "macos")] + // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding + { + "data:image/png;base64,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".into() + } + #[cfg(not(target_os = "macos"))] // 128x128 no padding + { + "data:image/png;base64,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".into() + } +} diff --git a/src/ui/cm.rs b/src/ui/cm.rs index cce553154..a574b5e88 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -100,7 +100,7 @@ impl SciterConnectionManager { } fn get_icon(&mut self) -> String { - crate::get_icon() + super::get_icon() } fn check_click_time(&mut self, id: i32) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 999b409e0..fdb6b2df8 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -486,7 +486,7 @@ impl SciterSession { } pub fn get_icon(&self) -> String { - crate::get_icon() + super::get_icon() } fn supported_hwcodec(&self) -> Value { From 7514a067d378f74a75b206fe86e0f1ed76f61a5b Mon Sep 17 00:00:00 2001 From: Carsten Date: Fri, 10 Feb 2023 21:32:21 +0100 Subject: [PATCH 030/202] Update README-DE.md fix grammar and improve readability --- docs/README-DE.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/README-DE.md b/docs/README-DE.md index 0b51d8fdd..e537d41f3 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -6,24 +6,24 @@ DateistrukturScreenshots
[English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
- Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren + Wir brauchen deine Hilfe, um diese README Datei zu verbessern und zu aktualisieren

-Rede mit uns: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Rede mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Das hier ist ein Programm was, man nutzen kann, um einen Computer fernzusteuern, es wurde in Rust geschrieben. Es funktioniert ohne Konfiguration oder ähnliches, man kann es einfach direkt nutzen. Du hast volle Kontrolle über deine Daten und brauchst dir daher auch keine Sorgen um die Sicherheit dieser Daten zu machen. Du kannst unseren Rendezvous/Relay Server nutzen, [einen eigenen Server eröffnen](https://rustdesk.com/server) oder [einen neuen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). +RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out-of-the-box ohne besondere Konfiguration funktioniert. Du hast die volle Kontrolle über deine Daten und musst dir keine Sorgen um die Sicherheit machen. Du kannst unseren Rendezvous/Relay Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Hilfe brauchst für den Start. +RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Unterstützung beim Start brauchst. [**PROGRAMM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) ## Kostenlose öffentliche Server -Hier sind die Server, die du kostenlos nutzen kannst, es kann sein das sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird. +Nachfolgend sind die Server gelistet, die du kostenlos nutzen kannst. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird. -| Standort | Serverart | Spezifikationen | Kommentare | +| Standort | Anbieter | Spezifikationen | Kommentar | | --------- | ------------- | ------------------ | ---------- | | Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | | | Germany | Codext | 2 vCPU / 4GB RAM | @@ -33,7 +33,7 @@ Hier sind die Server, die du kostenlos nutzen kannst, es kann sein das sich dies ## Abhängigkeiten -Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) für die Oberfläche, bitte lade die dynamische Sciter Bibliothek selbst herunter. +Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) oder Flutter für die GUI. Bitte lade die dynamische Sciter Bibliothek selbst herunter. [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) | @@ -41,7 +41,7 @@ Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) für die Oberfläche, ## Die groben Schritte zum Kompilieren -- Bereite deine Rust Entwicklungsumgebung und C++ Entwicklungsumgebung vor +- Bereite deine Rust Entwicklungsumgebung und C++ Build-Umgebung vor - Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die `VCPKG_ROOT` Systemumgebungsvariable hinzu @@ -110,11 +110,11 @@ cargo run ### Ändere Wayland zu X11 (Xorg) -RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) um Xorg als Standard GNOME Session zu nutzen. +RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard GNOME Session zu nutzen. -## Auf Docker Kompilieren +## Auf Docker kompilieren -Beginne damit das Repository zu klonen und den Docker Container zu bauen: +Beginne damit, das Repository zu klonen und den Docker Container zu bauen: ```sh git clone https://github.com/rustdesk/rustdesk @@ -122,13 +122,13 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -Jedes Mal, wenn du das Programm Kompilieren musst, nutze diesen Befehl: +Jedes Mal, wenn du das Programm kompilieren musst, nutze diesen Befehl: ```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 ``` -Bedenke, dass das erste Mal Kompilieren länger dauern kann, da die Abhängigkeiten erst kompiliert werden müssen bevor sie zwischengespeichert werden können. Darauf folgende Kompiliervorgänge werden schneller sein. Falls du zusätzliche oder andere Argumente für den Kompilierbefehl angeben musst, kannst du diese am Ende des Befehls an der `` Position machen. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du das tun, indem du `--release` am Ende des Befehls anhängst. Das daraus entstehende Programm kannst du im “target” Ordner auf deinem System finden. Du kannst es mit folgenden Befehlen ausführen: +Bedenke, dass das erste Mal Kompilieren länger dauern kann, da die Abhängigkeiten erst kompiliert werden müssen bevor sie zwischengespeichert werden können. Nachfolgende Kompiliervorgänge werden schneller sein. Falls du zusätzliche oder andere Argumente für den Kompilierbefehl angeben musst, kannst du diese am Ende des Befehls an der `` Position machen. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du das tun, indem du `--release` am Ende des Befehls anhängst. Das daraus entstehende Programm kannst du im “target” Ordner auf deinem System finden. Du kannst es mit folgenden Befehlen ausführen: ```sh target/debug/rustdesk @@ -140,13 +140,13 @@ Oder, wenn du eine Releaseversion benutzt: target/release/rustdesk ``` -Bitte gehe sicher, dass du diese Befehle vom Stammverzeichnis vom RustDesk Repository nutzt, sonst kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass Unterbefehle von Cargo, wie z. B. `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System. +Bitte stelle sicher, dass du diese Befehle vom Stammverzeichnis vom RustDesk Repository nutzt. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass Unterbefehle von Cargo, wie z. B. `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System. ## Dateistruktur -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer, und ein paar andere nützliche Funktionen +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer und ein paar andere nützliche Funktionen - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Bildschirmaufnahme -- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus und Tastatur Steuerung +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatur-Steuerung - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerk Verbindungen - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Starten einer Peer-Verbindung From 491932cda104b517ef236b27e026a603831f1400 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 11 Feb 2023 09:57:27 +0800 Subject: [PATCH 031/202] opt: fetch rgba positively for sessions on flutter --- flutter/lib/models/model.dart | 7 ++++++- src/flutter.rs | 8 +++++++- src/flutter_ffi.rs | 13 ++++++++++++- src/ui/remote.rs | 5 +++++ src/ui_session_interface.rs | 1 + 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index eb837ba70..f30209a60 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1376,7 +1376,12 @@ class FFI { debugPrint('json.decode fail1(): $e, ${message.field0}'); } } else if (message is EventToUI_Rgba) { - imageModel.onRgba(message.field0); + // Fetch the image buffer from rust codes. + bind.sessionGetRgba(id: id).then((rgba) { + if (rgba != null) { + imageModel.onRgba(rgba); + } + }); } } }(); diff --git a/src/flutter.rs b/src/flutter.rs index 7533244eb..8ef451397 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -110,6 +110,7 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, + pub rgba: Arc>>> } impl FlutterHandler { @@ -290,7 +291,8 @@ impl InvokeUiSession for FlutterHandler { fn on_rgba(&self, data: &[u8]) { if let Some(stream) = &*self.event_stream.read().unwrap() { - stream.add(EventToUI::Rgba(ZeroCopyBuffer(data.to_owned()))); + drop(self.rgba.write().unwrap().replace(data.to_owned())); + stream.add(EventToUI::Rgba); } } @@ -409,6 +411,10 @@ impl InvokeUiSession for FlutterHandler { fn on_voice_call_incoming(&self) { self.push_event("on_voice_call_incoming", [].into()); } + + fn get_rgba(&self) -> Option> { + self.rgba.write().unwrap().take() + } } /// Create a new remote session with the given id. diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a12d5acab..3a0fcc5fa 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -20,6 +20,7 @@ use std::{ os::raw::c_char, str::FromStr, }; +use crate::ui_session_interface::InvokeUiSession; // use crate::hbbs_http::account::AuthResult; @@ -47,7 +48,7 @@ fn initialize(app_dir: &str) { pub enum EventToUI { Event(String), - Rgba(ZeroCopyBuffer>), + Rgba, } pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { @@ -103,6 +104,16 @@ pub fn session_get_remember(id: String) -> Option { } } +pub fn session_get_rgba(id: String) -> Option>> { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + return match session.get_rgba() { + Some(buf) => Some(ZeroCopyBuffer(buf)), + _ => None + }; + } + None +} + pub fn session_get_toggle_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_toggle_option(arg)) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index fdb6b2df8..06af70eae 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -282,6 +282,11 @@ impl InvokeUiSession for SciterHandler { fn on_voice_call_incoming(&self) { self.call("onVoiceCallIncoming", &make_args!()); } + + /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. + fn get_rgba(&self) -> Option> { + None + } } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 87ea8e9eb..2944a76d1 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -722,6 +722,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); + fn get_rgba(&self) -> Option>; } impl Deref for Session { From f8c78a6bf2ca029d7b4fdc3523bb0b9ad4e3fbde Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 11 Feb 2023 10:14:09 +0800 Subject: [PATCH 032/202] opt: remove unnecessary rgba events to decrease memory usage --- src/flutter.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 8ef451397..a2dcbdbcf 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -291,8 +291,12 @@ impl InvokeUiSession for FlutterHandler { fn on_rgba(&self, data: &[u8]) { if let Some(stream) = &*self.event_stream.read().unwrap() { - drop(self.rgba.write().unwrap().replace(data.to_owned())); - stream.add(EventToUI::Rgba); + let former_rgba = self.rgba.write().unwrap().replace(data.to_owned()); + if former_rgba.is_none() { + // The [former_rgba] is none, which means the latest rgba had taken from flutter. + // We need to send a signal to flutter for notifying there's a new rgba buffer here. + stream.add(EventToUI::Rgba); + } } } From f521b1665a81f0e7dc11356fae993d7f26d3e4fb Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 11 Feb 2023 12:25:13 +0800 Subject: [PATCH 033/202] opt: no copy during transmitting the decoded frame --- src/client.rs | 4 ++-- src/flutter.rs | 6 +++--- src/ui/remote.rs | 4 ++-- src/ui_session_interface.rs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/client.rs b/src/client.rs index 020bea1f0..ecfc59749 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1545,7 +1545,7 @@ pub type MediaSender = mpsc::Sender; /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) where - F: 'static + FnMut(&[u8]) + Send, + F: 'static + FnMut(Vec) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); let mut video_callback = video_callback; @@ -1560,7 +1560,7 @@ where match data { MediaData::VideoFrame(vf) => { if let Ok(true) = video_handler.handle_frame(vf) { - video_callback(&video_handler.rgb); + video_callback(std::mem::replace(&mut video_handler.rgb, vec![])); } } MediaData::Reset => { diff --git a/src/flutter.rs b/src/flutter.rs index a2dcbdbcf..bee4dd7a5 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -3,7 +3,7 @@ use crate::{ flutter_ffi::EventToUI, ui_session_interface::{io_loop, InvokeUiSession, Session}, }; -use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; +use flutter_rust_bridge::{StreamSink}; use hbb_common::{ bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, ResultType, @@ -289,9 +289,9 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter fn adapt_size(&self) {} - fn on_rgba(&self, data: &[u8]) { + fn on_rgba(&self, data: Vec) { if let Some(stream) = &*self.event_stream.read().unwrap() { - let former_rgba = self.rgba.write().unwrap().replace(data.to_owned()); + let former_rgba = self.rgba.write().unwrap().replace(data); if former_rgba.is_none() { // The [former_rgba] is none, which means the latest rgba had taken from flutter. // We need to send a signal to flutter for notifying there's a new rgba buffer here. diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 06af70eae..b6663ad7e 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -201,12 +201,12 @@ impl InvokeUiSession for SciterHandler { self.call("adaptSize", &make_args!()); } - fn on_rgba(&self, data: &[u8]) { + fn on_rgba(&self, data: Vec) { VIDEO .lock() .unwrap() .as_mut() - .map(|v| v.render_frame(data).ok()); + .map(|v| v.render_frame(&data).ok()); } fn set_peer_info(&self, pi: &PeerInfo) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2944a76d1..cbf6d0171 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -712,7 +712,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn update_block_input_state(&self, on: bool); fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); fn adapt_size(&self); - fn on_rgba(&self, data: &[u8]); + fn on_rgba(&self, data: Vec); fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool); #[cfg(any(target_os = "android", target_os = "ios"))] fn clipboard(&self, content: String); @@ -957,7 +957,7 @@ pub async fn io_loop(handler: Session) { let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_cl = frame_count.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: &[u8]| { + let (video_sender, audio_sender) = start_video_audio_threads(move |data: Vec| { frame_count_cl.fetch_add(1, Ordering::Relaxed); ui_handler.on_rgba(data); }); From bf38fb7118321986b0cf502ab0809f742d74c3fb Mon Sep 17 00:00:00 2001 From: grummbeer Date: Sat, 11 Feb 2023 12:32:30 +0100 Subject: [PATCH 034/202] Dialog. Unify padding. --- flutter/lib/common.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a295ad4f8..6c1245a7d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -653,6 +653,7 @@ class CustomAlertDialog extends StatelessWidget { child: AlertDialog( scrollable: true, title: title, + titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0), contentPadding: EdgeInsets.fromLTRB( contentPadding ?? padding, 25, contentPadding ?? padding, 10), content: ConstrainedBox( From 01d30bce9e4509b6129843bf2d460d0351c28638 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 12 Feb 2023 01:52:11 +0800 Subject: [PATCH 035/202] opt: reduce copy and malloc times for both of flutter and rust --- flutter/lib/models/model.dart | 30 +++++++++++++--- flutter/lib/models/native_model.dart | 26 +++++++++++++- src/client.rs | 8 ++--- src/flutter.rs | 53 ++++++++++++++++++++++------ src/flutter_ffi.rs | 10 ------ src/ui/remote.rs | 10 +++--- src/ui_session_interface.rs | 6 ++-- 7 files changed, 105 insertions(+), 38 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f30209a60..e09a99875 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1,10 +1,12 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:ffi' hide Size; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:ffi/ffi.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/consts.dart'; @@ -1367,6 +1369,9 @@ class FFI { final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { + // Preserved for the rgba data. + Pointer? buffer; + int? bufferSize; await for (final message in stream) { if (message is EventToUI_Event) { try { @@ -1377,13 +1382,30 @@ class FFI { } } else if (message is EventToUI_Rgba) { // Fetch the image buffer from rust codes. - bind.sessionGetRgba(id: id).then((rgba) { - if (rgba != null) { - imageModel.onRgba(rgba); + final sz = platformFFI.getRgbaSize(id); + if (sz == null) { + return; + } + // The buffer does not exists or the bufferSize is not + // equal to the required size. + if (buffer == null || bufferSize != sz) { + // reallocate buffer + if (buffer != null) { + malloc.free(buffer); } - }); + buffer = malloc.allocate(sz); + bufferSize = sz; + } + final rgba = platformFFI.getRgba(id, buffer, bufferSize!); + if (rgba != null) { + imageModel.onRgba(rgba); + } } } + // Free the buffer allocated on the heap. + if (buffer != null) { + malloc.free(buffer); + } }(); // every instance will bind a stream this.id = id; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 34a673953..588c3646f 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -23,7 +23,10 @@ class RgbaFrame extends Struct { } typedef F2 = Pointer Function(Pointer, Pointer); -typedef F3 = void Function(Pointer, Pointer); +typedef F3 = Void Function(Pointer, Pointer); +typedef F3Dart = void Function(Pointer, Pointer); +typedef F4 = Uint64 Function(Pointer); +typedef F4Dart = int Function(Pointer); typedef HandleEvent = Future Function(Map evt); /// FFI wrapper around the native Rust core. @@ -44,6 +47,8 @@ class PlatformFFI { final _toAndroidChannel = const MethodChannel('mChannel'); RustdeskImpl get ffiBind => _ffiBind; + F3Dart? _session_get_rgba; + F4Dart? _session_get_rgba_size; static get localeName => Platform.localeName; @@ -92,6 +97,23 @@ class PlatformFFI { return res; } + Uint8List? getRgba(String id, Pointer buffer, int bufSize) { + if (_session_get_rgba == null) return null; + var a = id.toNativeUtf8(); + _session_get_rgba!(a, buffer); + final data = buffer.asTypedList(bufSize); + malloc.free(a); + return data; + } + + int? getRgbaSize(String id) { + if (_session_get_rgba_size == null) return null; + var a = id.toNativeUtf8(); + final bufferSize = _session_get_rgba_size!(a); + malloc.free(a); + return bufferSize; + } + /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { _appType = appType; @@ -107,6 +129,8 @@ class PlatformFFI { debugPrint('initializing FFI $_appType'); try { _translate = dylib.lookupFunction('translate'); + _session_get_rgba = dylib.lookupFunction("session_get_rgba"); + _session_get_rgba_size = dylib.lookupFunction("session_get_rgba_size"); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; diff --git a/src/client.rs b/src/client.rs index ecfc59749..c6e0a759f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -817,7 +817,7 @@ impl AudioHandler { pub struct VideoHandler { decoder: Decoder, latency_controller: Arc>, - pub rgb: Vec, + pub rgb: Arc>>, recorder: Arc>>, record: bool, } @@ -850,7 +850,7 @@ impl VideoHandler { } match &vf.union { Some(frame) => { - let res = self.decoder.handle_video_frame(frame, &mut self.rgb); + let res = self.decoder.handle_video_frame(frame, &mut self.rgb.write().unwrap()); if self.record { self.recorder .lock() @@ -1545,7 +1545,7 @@ pub type MediaSender = mpsc::Sender; /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) where - F: 'static + FnMut(Vec) + Send, + F: 'static + FnMut(Arc>>) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); let mut video_callback = video_callback; @@ -1560,7 +1560,7 @@ where match data { MediaData::VideoFrame(vf) => { if let Ok(true) = video_handler.handle_frame(vf) { - video_callback(std::mem::replace(&mut video_handler.rgb, vec![])); + video_callback(video_handler.rgb.clone()); } } MediaData::Reset => { diff --git a/src/flutter.rs b/src/flutter.rs index bee4dd7a5..bb6f85bb9 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -15,6 +15,7 @@ use std::{ os::raw::{c_char, c_int}, sync::{Arc, RwLock}, }; +use libc::memcpy; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_CM: &str = "cm"; @@ -110,7 +111,8 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, - pub rgba: Arc>>> + pub rgba: Arc>>, + pub rgba_valid: Arc> } impl FlutterHandler { @@ -289,15 +291,18 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter fn adapt_size(&self) {} - fn on_rgba(&self, data: Vec) { - if let Some(stream) = &*self.event_stream.read().unwrap() { - let former_rgba = self.rgba.write().unwrap().replace(data); - if former_rgba.is_none() { - // The [former_rgba] is none, which means the latest rgba had taken from flutter. - // We need to send a signal to flutter for notifying there's a new rgba buffer here. - stream.add(EventToUI::Rgba); - } + fn on_rgba(&self, data: Arc>>) { + // If the current rgba is not fetched by flutter, i.e., is valid. + // We give up sending a new event to flutter. + if *self.rgba_valid.read().unwrap() { + return; } + // Return the rgba buffer to the video handler for reusing allocated rgba buffer. + std::mem::swap::>(data.write().unwrap().as_mut(), self.rgba.write().unwrap().as_mut()); + if let Some(stream) = &*self.event_stream.read().unwrap() { + stream.add(EventToUI::Rgba); + } + let _ = std::mem::replace(&mut *self.rgba_valid.write().unwrap(), true); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -416,8 +421,13 @@ impl InvokeUiSession for FlutterHandler { self.push_event("on_voice_call_incoming", [].into()); } - fn get_rgba(&self) -> Option> { - self.rgba.write().unwrap().take() + fn get_rgba(&mut self, buffer: *mut u8) { + // [Safety] + // * It must be ensures the buffer has enough space to place the whole rgba. + let max_len = self.rgba.read().unwrap().len(); + unsafe { std::ptr::copy_nonoverlapping(self.rgba.read().unwrap().as_ptr(), buffer, max_len)}; + // mark the rgba has been taken from flutter. + let _ = std::mem::replace(&mut *self.rgba_valid.write().unwrap(), false); } } @@ -645,3 +655,24 @@ pub fn set_cur_session_id(id: String) { *CUR_SESSION_ID.write().unwrap() = id; } } + +#[no_mangle] +pub fn session_get_rgba_size(id: *const char) -> usize { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + return session.rgba.read().unwrap().len(); + } + } + 0 +} + +#[no_mangle] +pub fn session_get_rgba(id: *const char, buffer: *mut u8) { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + return session.get_rgba(buffer); + } + } +} \ No newline at end of file diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 3a0fcc5fa..b4e79b361 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -104,16 +104,6 @@ pub fn session_get_remember(id: String) -> Option { } } -pub fn session_get_rgba(id: String) -> Option>> { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - return match session.get_rgba() { - Some(buf) => Some(ZeroCopyBuffer(buf)), - _ => None - }; - } - None -} - pub fn session_get_toggle_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_toggle_option(arg)) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index b6663ad7e..ecf96ab32 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -3,6 +3,7 @@ use std::{ ops::{Deref, DerefMut}, sync::{Arc, Mutex}, }; +use std::sync::RwLock; use sciter::{ dom::{ @@ -17,6 +18,7 @@ use sciter::{ use hbb_common::{ allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType, }; +use hbb_common::tokio::io::AsyncReadExt; use crate::{ client::*, @@ -201,12 +203,12 @@ impl InvokeUiSession for SciterHandler { self.call("adaptSize", &make_args!()); } - fn on_rgba(&self, data: Vec) { + fn on_rgba(&self, data: Arc>>) { VIDEO .lock() .unwrap() .as_mut() - .map(|v| v.render_frame(&data).ok()); + .map(|v| v.render_frame(data.read().unwrap().as_ref()).ok()); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -284,9 +286,7 @@ impl InvokeUiSession for SciterHandler { } /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. - fn get_rgba(&self) -> Option> { - None - } + fn get_rgba(&mut self, _buffer: *mut u8) {} } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index cbf6d0171..85deb68c2 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -712,7 +712,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn update_block_input_state(&self, on: bool); fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); fn adapt_size(&self); - fn on_rgba(&self, data: Vec); + fn on_rgba(&self, data: Arc>>); fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool); #[cfg(any(target_os = "android", target_os = "ios"))] fn clipboard(&self, content: String); @@ -722,7 +722,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); - fn get_rgba(&self) -> Option>; + fn get_rgba(&mut self, buffer: *mut u8); } impl Deref for Session { @@ -957,7 +957,7 @@ pub async fn io_loop(handler: Session) { let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_cl = frame_count.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: Vec| { + let (video_sender, audio_sender) = start_video_audio_threads(move |data: Arc>> | { frame_count_cl.fetch_add(1, Ordering::Relaxed); ui_handler.on_rgba(data); }); From e0007788b1bec4af91bc286d46f3725c25614a65 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 12 Feb 2023 08:25:48 +0800 Subject: [PATCH 036/202] no blank issue, and make logo.svg compatible with flutter without inline style --- .github/ISSUE_TEMPLATE/config.yml | 1 + flutter/assets/logo.svg | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7b43e397b..2da6bbaf1 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,3 +1,4 @@ +blank_issues_enabled: false contact_links: - name: Ask a question url: https://github.com/rustdesk/rustdesk/discussions/category_choices diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg index 965218c95..d3a3f7b37 100644 --- a/flutter/assets/logo.svg +++ b/flutter/assets/logo.svg @@ -1 +1 @@ - \ No newline at end of file + From fbbb2cd4ff9ac856e5511b3b6de796197caafdfe Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 12 Feb 2023 08:49:09 +0800 Subject: [PATCH 037/202] fix another svg compatibility, move def back, to make href can find --- flutter/assets/logo.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg index d3a3f7b37..13eb73f22 100644 --- a/flutter/assets/logo.svg +++ b/flutter/assets/logo.svg @@ -1 +1 @@ - + From 3d40569dee56c903b481f3ab27108f524bb74e6c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 12 Feb 2023 09:03:13 +0800 Subject: [PATCH 038/202] change all ocusNode: FocusNode()..requestFocus(), to autofocus: true`` --- flutter/lib/common/widgets/address_book.dart | 2 +- flutter/lib/common/widgets/dialog.dart | 6 +++--- flutter/lib/common/widgets/peer_card.dart | 4 ++-- flutter/lib/desktop/pages/desktop_home_page.dart | 2 +- flutter/lib/desktop/pages/file_manager_page.dart | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 5cd2af2be..bd2a01296 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -386,7 +386,7 @@ class _AddressBookState extends State { errorText: msg.isEmpty ? null : translate(msg), ), controller: controller, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ), ], diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 837a197dc..e96a2b406 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -54,7 +54,7 @@ void changeIdDialog() { ], maxLength: 16, controller: controller, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), const SizedBox( height: 4.0, @@ -99,7 +99,7 @@ void changeWhiteList({Function()? callback}) async { errorText: msg.isEmpty ? null : translate(msg), ), controller: controller, - focusNode: FocusNode()..requestFocus()), + autofocus: true), ), ], ), @@ -186,7 +186,7 @@ Future changeDirectAccessPort( r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), ], controller: controller, - focusNode: FocusNode()..requestFocus()), + autofocus: true), ), ], ), diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index c9af6328c..3c9a438a0 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -641,7 +641,7 @@ abstract class BasePeerCard extends StatelessWidget { child: Form( child: TextFormField( controller: controller, - focusNode: FocusNode()..requestFocus(), + autofocus: true, decoration: const InputDecoration(border: OutlineInputBorder()), ), @@ -1013,7 +1013,7 @@ void _rdpDialog(String id) async { decoration: const InputDecoration( border: OutlineInputBorder(), hintText: '3389'), controller: portController, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ), ], diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 2986adc7a..cde1e6d74 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -634,7 +634,7 @@ void setPasswordDialog() async { border: const OutlineInputBorder(), errorText: errMsg0.isNotEmpty ? errMsg0 : null), controller: p0, - focusNode: FocusNode()..requestFocus(), + autofocus: true, onChanged: (value) { rxPass.value = value.trim(); }, diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 9955c2768..27bb0377d 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -798,7 +798,7 @@ class _FileManagerPageState extends State "Please enter the folder name"), ), controller: name, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ], ), From d2e24173d0d87e840e41e22dc1a74b588322979e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 12 Feb 2023 10:28:04 +0800 Subject: [PATCH 039/202] opt: read uint8list directly from rust codes --- flutter/lib/models/model.dart | 30 +++--------------- flutter/lib/models/native_model.dart | 39 +++++++++++++++++------ src/client.rs | 8 ++--- src/flutter.rs | 47 +++++++++++++++++++--------- src/ui/remote.rs | 8 +++-- src/ui_session_interface.rs | 7 +++-- 6 files changed, 78 insertions(+), 61 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e09a99875..8cf90eba9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -417,8 +417,6 @@ class ImageModel with ChangeNotifier { String id = ''; - int decodeCount = 0; - WeakReference parent; final List _callbacksOnFirstImage = []; @@ -439,20 +437,16 @@ class ImageModel with ChangeNotifier { } } - if (decodeCount >= 1) { - return; - } - final pid = parent.target?.id; - decodeCount += 1; ui.decodeImageFromPixels( rgba, parent.target?.ffiModel.display.width ?? 0, parent.target?.ffiModel.display.height ?? 0, isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { - decodeCount -= 1; if (parent.target?.id != pid) return; try { + // Unlock the rgba memory from rust codes. + platformFFI.nextRgba(id); // my throw exception, because the listener maybe already dispose update(image); } catch (e) { @@ -1370,8 +1364,6 @@ class FFI { final cb = ffiModel.startEventListener(id); () async { // Preserved for the rgba data. - Pointer? buffer; - int? bufferSize; await for (final message in stream) { if (message is EventToUI_Event) { try { @@ -1383,29 +1375,15 @@ class FFI { } else if (message is EventToUI_Rgba) { // Fetch the image buffer from rust codes. final sz = platformFFI.getRgbaSize(id); - if (sz == null) { + if (sz == null || sz == 0) { return; } - // The buffer does not exists or the bufferSize is not - // equal to the required size. - if (buffer == null || bufferSize != sz) { - // reallocate buffer - if (buffer != null) { - malloc.free(buffer); - } - buffer = malloc.allocate(sz); - bufferSize = sz; - } - final rgba = platformFFI.getRgba(id, buffer, bufferSize!); + final rgba = platformFFI.getRgba(id, sz); if (rgba != null) { imageModel.onRgba(rgba); } } } - // Free the buffer allocated on the heap. - if (buffer != null) { - malloc.free(buffer); - } }(); // every instance will bind a stream this.id = id; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 588c3646f..ba62b775e 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -9,6 +9,7 @@ import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/consts.dart'; +import 'package:get/get.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:win32/win32.dart' as win32; @@ -23,10 +24,11 @@ class RgbaFrame extends Struct { } typedef F2 = Pointer Function(Pointer, Pointer); -typedef F3 = Void Function(Pointer, Pointer); -typedef F3Dart = void Function(Pointer, Pointer); +typedef F3 = Pointer Function(Pointer); typedef F4 = Uint64 Function(Pointer); typedef F4Dart = int Function(Pointer); +typedef F5 = Void Function(Pointer); +typedef F5Dart = void Function(Pointer); typedef HandleEvent = Future Function(Map evt); /// FFI wrapper around the native Rust core. @@ -47,8 +49,9 @@ class PlatformFFI { final _toAndroidChannel = const MethodChannel('mChannel'); RustdeskImpl get ffiBind => _ffiBind; - F3Dart? _session_get_rgba; + F3? _session_get_rgba; F4Dart? _session_get_rgba_size; + F5Dart? _session_next_rgba; static get localeName => Platform.localeName; @@ -97,13 +100,19 @@ class PlatformFFI { return res; } - Uint8List? getRgba(String id, Pointer buffer, int bufSize) { + Uint8List? getRgba(String id, int bufSize) { if (_session_get_rgba == null) return null; var a = id.toNativeUtf8(); - _session_get_rgba!(a, buffer); - final data = buffer.asTypedList(bufSize); - malloc.free(a); - return data; + try { + final buffer = _session_get_rgba!(a); + if (buffer == nullptr) { + return null; + } + final data = buffer.asTypedList(bufSize); + return data; + } finally { + malloc.free(a); + } } int? getRgbaSize(String id) { @@ -114,6 +123,13 @@ class PlatformFFI { return bufferSize; } + void nextRgba(String id) { + if (_session_next_rgba == null) return; + final a = id.toNativeUtf8(); + _session_next_rgba!(a); + malloc.free(a); + } + /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { _appType = appType; @@ -129,8 +145,11 @@ class PlatformFFI { debugPrint('initializing FFI $_appType'); try { _translate = dylib.lookupFunction('translate'); - _session_get_rgba = dylib.lookupFunction("session_get_rgba"); - _session_get_rgba_size = dylib.lookupFunction("session_get_rgba_size"); + _session_get_rgba = dylib.lookupFunction("session_get_rgba"); + _session_get_rgba_size = + dylib.lookupFunction("session_get_rgba_size"); + _session_next_rgba = + dylib.lookupFunction("session_next_rgba"); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; diff --git a/src/client.rs b/src/client.rs index c6e0a759f..a21592578 100644 --- a/src/client.rs +++ b/src/client.rs @@ -817,7 +817,7 @@ impl AudioHandler { pub struct VideoHandler { decoder: Decoder, latency_controller: Arc>, - pub rgb: Arc>>, + pub rgb: Vec, recorder: Arc>>, record: bool, } @@ -850,7 +850,7 @@ impl VideoHandler { } match &vf.union { Some(frame) => { - let res = self.decoder.handle_video_frame(frame, &mut self.rgb.write().unwrap()); + let res = self.decoder.handle_video_frame(frame, &mut self.rgb); if self.record { self.recorder .lock() @@ -1545,7 +1545,7 @@ pub type MediaSender = mpsc::Sender; /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) where - F: 'static + FnMut(Arc>>) + Send, + F: 'static + FnMut(&mut Vec) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); let mut video_callback = video_callback; @@ -1560,7 +1560,7 @@ where match data { MediaData::VideoFrame(vf) => { if let Ok(true) = video_handler.handle_frame(vf) { - video_callback(video_handler.rgb.clone()); + video_callback(&mut video_handler.rgb); } } MediaData::Reset => { diff --git a/src/flutter.rs b/src/flutter.rs index bb6f85bb9..a60e379f9 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -15,7 +15,7 @@ use std::{ os::raw::{c_char, c_int}, sync::{Arc, RwLock}, }; -use libc::memcpy; +use std::sync::atomic::{AtomicBool, Ordering}; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_CM: &str = "cm"; @@ -111,8 +111,10 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, + // SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`. + // We must check the `rgba_valid` before reading [rgba]. pub rgba: Arc>>, - pub rgba_valid: Arc> + pub rgba_valid: Arc } impl FlutterHandler { @@ -291,18 +293,18 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter fn adapt_size(&self) {} - fn on_rgba(&self, data: Arc>>) { + fn on_rgba(&self, data: &mut Vec) { // If the current rgba is not fetched by flutter, i.e., is valid. // We give up sending a new event to flutter. - if *self.rgba_valid.read().unwrap() { + if self.rgba_valid.load(Ordering::Relaxed) { return; } + self.rgba_valid.store(true, Ordering::Relaxed); // Return the rgba buffer to the video handler for reusing allocated rgba buffer. - std::mem::swap::>(data.write().unwrap().as_mut(), self.rgba.write().unwrap().as_mut()); + std::mem::swap::>(data, &mut *self.rgba.write().unwrap()); if let Some(stream) = &*self.event_stream.read().unwrap() { stream.add(EventToUI::Rgba); } - let _ = std::mem::replace(&mut *self.rgba_valid.write().unwrap(), true); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -421,13 +423,17 @@ impl InvokeUiSession for FlutterHandler { self.push_event("on_voice_call_incoming", [].into()); } - fn get_rgba(&mut self, buffer: *mut u8) { - // [Safety] - // * It must be ensures the buffer has enough space to place the whole rgba. - let max_len = self.rgba.read().unwrap().len(); - unsafe { std::ptr::copy_nonoverlapping(self.rgba.read().unwrap().as_ptr(), buffer, max_len)}; - // mark the rgba has been taken from flutter. - let _ = std::mem::replace(&mut *self.rgba_valid.write().unwrap(), false); + #[inline] + fn get_rgba(&self) -> *const u8 { + if self.rgba_valid.load(Ordering::Relaxed) { + return self.rgba.read().unwrap().as_ptr(); + } + std::ptr::null_mut() + } + + #[inline] + fn next_rgba(&mut self) { + self.rgba_valid.store(false, Ordering::Relaxed); } } @@ -668,11 +674,22 @@ pub fn session_get_rgba_size(id: *const char) -> usize { } #[no_mangle] -pub fn session_get_rgba(id: *const char, buffer: *mut u8) { +pub fn session_get_rgba(id: *const char) -> *const u8 { let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; if let Ok(id) = id.to_str() { if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { - return session.get_rgba(buffer); + return session.get_rgba(); + } + } + std::ptr::null() +} + +#[no_mangle] +pub fn session_next_rgba(id: *const char) { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + return session.next_rgba(); } } } \ No newline at end of file diff --git a/src/ui/remote.rs b/src/ui/remote.rs index ecf96ab32..e44e31401 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -203,12 +203,12 @@ impl InvokeUiSession for SciterHandler { self.call("adaptSize", &make_args!()); } - fn on_rgba(&self, data: Arc>>) { + fn on_rgba(&self, data: &mut Vec) { VIDEO .lock() .unwrap() .as_mut() - .map(|v| v.render_frame(data.read().unwrap().as_ref()).ok()); + .map(|v| v.render_frame(data).ok()); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -286,7 +286,9 @@ impl InvokeUiSession for SciterHandler { } /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. - fn get_rgba(&mut self, _buffer: *mut u8) {} + fn get_rgba(&self) -> *const u8 { std::ptr::null() } + + fn next_rgba(&mut self) {} } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 85deb68c2..25c15f52f 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -712,7 +712,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn update_block_input_state(&self, on: bool); fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); fn adapt_size(&self); - fn on_rgba(&self, data: Arc>>); + fn on_rgba(&self, data: &mut Vec); fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool); #[cfg(any(target_os = "android", target_os = "ios"))] fn clipboard(&self, content: String); @@ -722,7 +722,8 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); - fn get_rgba(&mut self, buffer: *mut u8); + fn get_rgba(&self) -> *const u8; + fn next_rgba(&mut self); } impl Deref for Session { @@ -957,7 +958,7 @@ pub async fn io_loop(handler: Session) { let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_cl = frame_count.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: Arc>> | { + let (video_sender, audio_sender) = start_video_audio_threads(move |data: &mut Vec | { frame_count_cl.fetch_add(1, Ordering::Relaxed); ui_handler.on_rgba(data); }); From 9fb5b2cb5f9511c2b4754fa1a18cacd1cce1922d Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 12 Feb 2023 21:26:04 +0900 Subject: [PATCH 040/202] use flutter_keyboard_visibility --- flutter/lib/mobile/pages/remote_page.dart | 71 +++++++++-------------- flutter/pubspec.lock | 48 +++++++++++++++ flutter/pubspec.yaml | 1 + 3 files changed, 75 insertions(+), 45 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 54b6f1d47..d1faa5494 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; @@ -33,10 +34,8 @@ class RemotePage extends StatefulWidget { } class _RemotePageState extends State { - Timer? _interval; Timer? _timer; bool _showBar = !isWebDesktop; - double _bottom = 0; String _value = ''; double _scale = 1; double _mouseScrollIntegral = 0; // mouse scroll speed controller @@ -44,6 +43,8 @@ class _RemotePageState extends State { var _more = true; var _fn = false; + late final keyboardVisibilityController = KeyboardVisibilityController(); + late final StreamSubscription keyboardSubscription; final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); var _showEdit = false; // use soft keyboard @@ -58,14 +59,14 @@ class _RemotePageState extends State { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); gFFI.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); - _interval = - Timer.periodic(Duration(milliseconds: 30), (timer) => interval()); }); Wakelock.enable(); _physicalFocusNode.requestFocus(); gFFI.ffiModel.updateEventListener(widget.id); gFFI.inputModel.listenToMouse(true); gFFI.qualityMonitorModel.checkShowQualityMonitor(widget.id); + keyboardSubscription = + keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged); } @override @@ -76,49 +77,27 @@ class _RemotePageState extends State { _mobileFocusNode.dispose(); _physicalFocusNode.dispose(); gFFI.close(); - _interval?.cancel(); _timer?.cancel(); gFFI.dialogManager.dismissAll(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); Wakelock.disable(); + keyboardSubscription.cancel(); super.dispose(); } - void resetTool() { + void onSoftKeyboardChanged(bool visible) { inputModel.resetModifiers(); - } - - bool isKeyboardShown() { - return _bottom >= 100; - } - - // crash on web before widget initiated. - void intervalUnsafe() { - var v = MediaQuery.of(context).viewInsets.bottom; - if (v != _bottom) { - resetTool(); - setState(() { - _bottom = v; - if (v < 100) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, - overlays: []); - // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard - if (gFFI.chatModel.chatWindowOverlayEntry == null && - gFFI.ffiModel.pi.version.isNotEmpty) { - gFFI.invokeMethod("enable_soft_keyboard", false); - } - } - }); + if (!visible) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard + if (gFFI.chatModel.chatWindowOverlayEntry == null && + gFFI.ffiModel.pi.version.isNotEmpty) { + gFFI.invokeMethod("enable_soft_keyboard", false); + } } } - void interval() { - try { - intervalUnsafe(); - } catch (e) {} - } - // handle mobile virtual keyboard void handleSoftKeyboardInput(String newValue) { var oldValue = _value; @@ -219,8 +198,9 @@ class _RemotePageState extends State { @override Widget build(BuildContext context) { final pi = Provider.of(context).pi; - final hideKeyboard = isKeyboardShown() && _showEdit; - final showActionButton = !_showBar || hideKeyboard; + final isHideKeyboardFAB = + keyboardVisibilityController.isVisible && _showEdit; + final showActionButton = !_showBar || isHideKeyboardFAB; final keyboard = gFFI.ffiModel.permissions['keyboard'] != false; return WillPopScope( @@ -230,21 +210,21 @@ class _RemotePageState extends State { }, child: getRawPointerAndKeyBody(Scaffold( // workaround for https://github.com/rustdesk/rustdesk/issues/3131 - floatingActionButtonLocation: hideKeyboard + floatingActionButtonLocation: isHideKeyboardFAB ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) : null, floatingActionButton: !showActionButton ? null : FloatingActionButton( - mini: !hideKeyboard, + mini: !isHideKeyboardFAB, child: Icon( - hideKeyboard ? Icons.expand_more : Icons.expand_less, + isHideKeyboardFAB ? Icons.expand_more : Icons.expand_less, color: Colors.white, ), backgroundColor: MyTheme.accent, onPressed: () { setState(() { - if (hideKeyboard) { + if (isHideKeyboardFAB) { _showEdit = false; gFFI.invokeMethod("enable_soft_keyboard", false); _mobileFocusNode.unfocus(); @@ -725,7 +705,7 @@ class _RemotePageState extends State { // } Widget getHelpTools() { - final keyboard = isKeyboardShown(); + final keyboard = keyboardVisibilityController.isVisible; if (!keyboard) { return SizedBox(); } @@ -858,9 +838,10 @@ class _RemotePageState extends State { spacing: space, runSpacing: space, children: [SizedBox(width: 9999)] + - (keyboard - ? modifiers + keys + (_fn ? fn : []) + (_more ? more : []) - : modifiers), + modifiers + + keys + + (_fn ? fn : []) + + (_more ? more : []), )); } } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index cd618dfc4..91a061fb9 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -488,6 +488,54 @@ packages: url: "https://github.com/Kingtous/flutter_improved_scrolling" source: git version: "0.0.3" + flutter_keyboard_visibility: + dependency: "direct main" + description: + name: flutter_keyboard_visibility + sha256: "86b71bbaffa38e885f5c21b1182408b9be6951fd125432cf6652c636254cef2d" + url: "https://pub.dev" + source: hosted + version: "5.4.0" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_launcher_icons: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 8701d9f5b..df29252c9 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -91,6 +91,7 @@ dependencies: win32: any password_strength: ^0.2.0 flutter_launcher_icons: ^0.11.0 + flutter_keyboard_visibility: ^5.4.0 dev_dependencies: From 6e4e463f5f28e5c819e46570e12bb2e2a867ccc1 Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 12 Feb 2023 22:03:43 +0900 Subject: [PATCH 041/202] update HelpTools, use StatefulWidget --- flutter/lib/mobile/pages/remote_page.dart | 74 ++++++++++++++--------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index d1faa5494..1ec57b46e 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -41,8 +41,6 @@ class _RemotePageState extends State { double _mouseScrollIntegral = 0; // mouse scroll speed controller Orientation? _currentOrientation; - var _more = true; - var _fn = false; late final keyboardVisibilityController = KeyboardVisibilityController(); late final StreamSubscription keyboardSubscription; final FocusNode _mobileFocusNode = FocusNode(); @@ -96,6 +94,8 @@ class _RemotePageState extends State { gFFI.invokeMethod("enable_soft_keyboard", false); } } + // update for Scaffold + setState(() {}); } // handle mobile virtual keyboard @@ -478,6 +478,7 @@ class _RemotePageState extends State { } Widget getBodyForMobile() { + final keyboardIsVisible = keyboardVisibilityController.isVisible; return Container( color: MyTheme.canvasColor, child: Stack(children: () { @@ -488,7 +489,7 @@ class _RemotePageState extends State { right: 10, child: QualityMonitor(gFFI.qualityMonitorModel), ), - getHelpTools(), + KeyHelpTools(requestShow: keyboardIsVisible), SizedBox( width: 0, height: 0, @@ -703,33 +704,51 @@ class _RemotePageState extends State { // ])); // }, clickMaskDismiss: true); // } +} - Widget getHelpTools() { - final keyboard = keyboardVisibilityController.isVisible; - if (!keyboard) { +class KeyHelpTools extends StatefulWidget { + /// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode] + final bool requestShow; + + KeyHelpTools({required this.requestShow}); + + @override + State createState() => _KeyHelpToolsState(); +} + +class _KeyHelpToolsState extends State { + var _more = true; + var _fn = false; + + InputModel get inputModel => gFFI.inputModel; + + Widget wrap(String text, void Function() onPressed, + [bool? active, IconData? icon]) { + return TextButton( + style: TextButton.styleFrom( + minimumSize: Size(0, 0), + padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75), + //adds padding inside the button + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + //limits the touch area to the button area + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0), + ), + backgroundColor: active == true ? MyTheme.accent80 : null, + ), + child: icon != null + ? Icon(icon, size: 17, color: Colors.white) + : Text(translate(text), + style: TextStyle(color: Colors.white, fontSize: 11)), + onPressed: onPressed); + } + + @override + Widget build(BuildContext context) { + if (!widget.requestShow) { return SizedBox(); } final size = MediaQuery.of(context).size; - wrap(String text, void Function() onPressed, - [bool? active, IconData? icon]) { - return TextButton( - style: TextButton.styleFrom( - minimumSize: Size(0, 0), - padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75), - //adds padding inside the button - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - //limits the touch area to the button area - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5.0), - ), - backgroundColor: active == true ? MyTheme.accent80 : null, - ), - child: icon != null - ? Icon(icon, size: 17, color: Colors.white) - : Text(translate(text), - style: TextStyle(color: Colors.white, fontSize: 11)), - onPressed: onPressed); - } final pi = gFFI.ffiModel.pi; final isMac = pi.platform == kPeerPlatformMacOS; @@ -832,8 +851,7 @@ class _RemotePageState extends State { final space = size.width > 320 ? 4.0 : 2.0; return Container( color: Color(0xAA000000), - padding: EdgeInsets.only( - top: keyboard ? 24 : 4, left: 0, right: 0, bottom: 8), + padding: EdgeInsets.only(top: widget.requestShow ? 24 : 4, bottom: 8), child: Wrap( spacing: space, runSpacing: space, From 4b52431dbf295b1d71361335ddcb6838a48c2c2e Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 12 Feb 2023 22:20:51 +0900 Subject: [PATCH 042/202] KeyHelpTools add pin , and keep enable when hasModifierOn --- flutter/lib/mobile/pages/remote_page.dart | 42 +++++++++++++++-------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 1ec57b46e..63a289c95 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -85,7 +85,6 @@ class _RemotePageState extends State { } void onSoftKeyboardChanged(bool visible) { - inputModel.resetModifiers(); if (!visible) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard @@ -719,11 +718,12 @@ class KeyHelpTools extends StatefulWidget { class _KeyHelpToolsState extends State { var _more = true; var _fn = false; + var _pin = false; InputModel get inputModel => gFFI.inputModel; Widget wrap(String text, void Function() onPressed, - [bool? active, IconData? icon]) { + {bool? active, IconData? icon}) { return TextButton( style: TextButton.styleFrom( minimumSize: Size(0, 0), @@ -737,7 +737,7 @@ class _KeyHelpToolsState extends State { backgroundColor: active == true ? MyTheme.accent80 : null, ), child: icon != null - ? Icon(icon, size: 17, color: Colors.white) + ? Icon(icon, size: 14, color: Colors.white) : Text(translate(text), style: TextStyle(color: Colors.white, fontSize: 11)), onPressed: onPressed); @@ -745,8 +745,13 @@ class _KeyHelpToolsState extends State { @override Widget build(BuildContext context) { - if (!widget.requestShow) { - return SizedBox(); + final hasModifierOn = inputModel.ctrl || + inputModel.alt || + inputModel.shift || + inputModel.command; + + if (!_pin && !hasModifierOn && !widget.requestShow) { + return Offstage(); } final size = MediaQuery.of(context).size; @@ -755,16 +760,16 @@ class _KeyHelpToolsState extends State { final modifiers = [ wrap('Ctrl ', () { setState(() => inputModel.ctrl = !inputModel.ctrl); - }, inputModel.ctrl), + }, active: inputModel.ctrl), wrap(' Alt ', () { setState(() => inputModel.alt = !inputModel.alt); - }, inputModel.alt), + }, active: inputModel.alt), wrap('Shift', () { setState(() => inputModel.shift = !inputModel.shift); - }, inputModel.shift), + }, active: inputModel.shift), wrap(isMac ? ' Cmd ' : ' Win ', () { setState(() => inputModel.command = !inputModel.command); - }, inputModel.command), + }, active: inputModel.command), ]; final keys = [ wrap( @@ -777,7 +782,14 @@ class _KeyHelpToolsState extends State { } }, ), - _fn), + active: _fn), + wrap( + '', + () => setState( + () => _pin = !_pin, + ), + active: _pin, + icon: Icons.push_pin), wrap( ' ... ', () => setState( @@ -788,7 +800,7 @@ class _KeyHelpToolsState extends State { } }, ), - _more), + active: _more), ]; final fn = [ SizedBox(width: 9999), @@ -828,16 +840,16 @@ class _KeyHelpToolsState extends State { SizedBox(width: 9999), wrap('', () { inputModel.inputKey('VK_LEFT'); - }, false, Icons.keyboard_arrow_left), + }, icon: Icons.keyboard_arrow_left), wrap('', () { inputModel.inputKey('VK_UP'); - }, false, Icons.keyboard_arrow_up), + }, icon: Icons.keyboard_arrow_up), wrap('', () { inputModel.inputKey('VK_DOWN'); - }, false, Icons.keyboard_arrow_down), + }, icon: Icons.keyboard_arrow_down), wrap('', () { inputModel.inputKey('VK_RIGHT'); - }, false, Icons.keyboard_arrow_right), + }, icon: Icons.keyboard_arrow_right), wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () { sendPrompt(isMac, 'VK_C'); }), From 14a187f47105ae2d60ec6b91ae36a65894732be8 Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 12 Feb 2023 22:44:53 +0900 Subject: [PATCH 043/202] change GestureHelp from ModalBottomSheet to bottomNavigationBar, add show KeyTools when GestureHelp showed --- flutter/lib/mobile/pages/remote_page.dart | 73 ++++++++++++----------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 63a289c95..951d63faf 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -36,12 +36,13 @@ class RemotePage extends StatefulWidget { class _RemotePageState extends State { Timer? _timer; bool _showBar = !isWebDesktop; + bool _showGestureHelp = false; String _value = ''; double _scale = 1; double _mouseScrollIntegral = 0; // mouse scroll speed controller Orientation? _currentOrientation; - late final keyboardVisibilityController = KeyboardVisibilityController(); + final keyboardVisibilityController = KeyboardVisibilityController(); late final StreamSubscription keyboardSubscription; final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); @@ -197,9 +198,9 @@ class _RemotePageState extends State { @override Widget build(BuildContext context) { final pi = Provider.of(context).pi; - final isHideKeyboardFAB = + final keyboardIsVisible = keyboardVisibilityController.isVisible && _showEdit; - final showActionButton = !_showBar || isHideKeyboardFAB; + final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp; final keyboard = gFFI.ffiModel.permissions['keyboard'] != false; return WillPopScope( @@ -209,33 +210,39 @@ class _RemotePageState extends State { }, child: getRawPointerAndKeyBody(Scaffold( // workaround for https://github.com/rustdesk/rustdesk/issues/3131 - floatingActionButtonLocation: isHideKeyboardFAB + floatingActionButtonLocation: keyboardIsVisible ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) : null, floatingActionButton: !showActionButton ? null : FloatingActionButton( - mini: !isHideKeyboardFAB, + mini: !keyboardIsVisible, child: Icon( - isHideKeyboardFAB ? Icons.expand_more : Icons.expand_less, + (keyboardIsVisible || _showGestureHelp) + ? Icons.expand_more + : Icons.expand_less, color: Colors.white, ), backgroundColor: MyTheme.accent, onPressed: () { setState(() { - if (isHideKeyboardFAB) { + if (keyboardIsVisible) { _showEdit = false; gFFI.invokeMethod("enable_soft_keyboard", false); _mobileFocusNode.unfocus(); _physicalFocusNode.requestFocus(); + } else if (_showGestureHelp) { + _showGestureHelp = false; } else { _showBar = !_showBar; } }); }), - bottomNavigationBar: _showBar && pi.displays.isNotEmpty - ? getBottomAppBar(keyboard) - : null, + bottomNavigationBar: _showGestureHelp + ? getGestureHelp() + : (_showBar && pi.displays.isNotEmpty + ? getBottomAppBar(keyboard) + : null), body: Overlay( initialEntries: [ OverlayEntry(builder: (context) { @@ -325,7 +332,8 @@ class _RemotePageState extends State { icon: Icon(gFFI.ffiModel.touchMode ? Icons.touch_app : Icons.mouse), - onPressed: changeTouchMode, + onPressed: () => setState( + () => _showGestureHelp = !_showGestureHelp), ), ]) + (isWeb @@ -488,7 +496,7 @@ class _RemotePageState extends State { right: 10, child: QualityMonitor(gFFI.qualityMonitorModel), ), - KeyHelpTools(requestShow: keyboardIsVisible), + KeyHelpTools(requestShow: (keyboardIsVisible || _showGestureHelp)), SizedBox( width: 0, height: 0, @@ -658,29 +666,20 @@ class _RemotePageState extends State { }(); } - void changeTouchMode() { - setState(() => _showEdit = false); - showModalBottomSheet( - // backgroundColor: MyTheme.grayBg, - isScrollControlled: true, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(5))), - builder: (context) => DraggableScrollableSheet( - expand: false, - builder: (context, scrollController) { - return SingleChildScrollView( - controller: ScrollController(), - padding: EdgeInsets.symmetric(vertical: 10), - child: GestureHelp( - touchMode: gFFI.ffiModel.touchMode, - onTouchModeChange: (t) { - gFFI.ffiModel.toggleTouchMode(); - final v = gFFI.ffiModel.touchMode ? 'Y' : ''; - bind.sessionPeerOption( - id: widget.id, name: "touch", value: v); - })); - })); + /// aka changeTouchMode + BottomAppBar getGestureHelp() { + return BottomAppBar( + child: SingleChildScrollView( + controller: ScrollController(), + padding: EdgeInsets.symmetric(vertical: 10), + child: GestureHelp( + touchMode: gFFI.ffiModel.touchMode, + onTouchModeChange: (t) { + gFFI.ffiModel.toggleTouchMode(); + final v = gFFI.ffiModel.touchMode ? 'Y' : ''; + bind.sessionPeerOption( + id: widget.id, name: "touch", value: v); + }))); } // * Currently mobile does not enable map mode @@ -719,6 +718,7 @@ class _KeyHelpToolsState extends State { var _more = true; var _fn = false; var _pin = false; + final _keyboardVisibilityController = KeyboardVisibilityController(); InputModel get inputModel => gFFI.inputModel; @@ -863,7 +863,8 @@ class _KeyHelpToolsState extends State { final space = size.width > 320 ? 4.0 : 2.0; return Container( color: Color(0xAA000000), - padding: EdgeInsets.only(top: widget.requestShow ? 24 : 4, bottom: 8), + padding: EdgeInsets.only( + top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8), child: Wrap( spacing: space, runSpacing: space, From 0ecc35dcb3e4e0e89f8d0405ef8dc230180cace8 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 13 Feb 2023 15:12:05 +0800 Subject: [PATCH 044/202] opt: fix codesign with strict and verbose mode --- .github/workflows/flutter-nightly.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index f03cd0be8..1ab21dbff 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -242,9 +242,9 @@ jobs: security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain # start sign the rustdesk.app and dmg rm rustdesk-${{ env.VERSION }}.dmg || true - codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/RustDesk.app -v + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app - codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv # notarize the rustdesk-${{ env.VERSION }}.dmg rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg From d45224dfd8ee487263532e33c0cc707361306f45 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 13 Feb 2023 16:04:47 +0800 Subject: [PATCH 045/202] refactor login error message Signed-off-by: fufesou --- flutter/lib/common/widgets/login.dart | 33 ++++++++++++++------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 05fc1fc5c..14a2c38bc 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -197,24 +197,25 @@ class _WidgetOPState extends State { _failedMsg = ''; } return Offstage( - offstage: - _failedMsg.isEmpty && widget.curOP.value != widget.config.op, - child: Row( - children: [ - Text( - _stateMsg, - style: TextStyle(fontSize: 12), - ), - SizedBox(width: 8), - Text( - _failedMsg, - style: TextStyle( - fontSize: 14, - color: Colors.red, - ), + offstage: + _failedMsg.isEmpty && widget.curOP.value != widget.config.op, + child: RichText( + text: TextSpan( + text: '$_stateMsg ', + style: + DefaultTextStyle.of(context).style.copyWith(fontSize: 12), + children: [ + TextSpan( + text: _failedMsg, + style: DefaultTextStyle.of(context).style.copyWith( + fontSize: 14, + color: Colors.red, + ), ), ], - )); + ), + ), + ); }), Obx( () => Offstage( From 9492f401f4e147b0c28f46392e075c78d1da7644 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 13 Feb 2023 16:18:46 +0800 Subject: [PATCH 046/202] fix: allowing idle scroll events --- flutter/lib/common.dart | 25 +++++++++++++++++++ .../lib/desktop/pages/connection_page.dart | 2 +- .../lib/desktop/pages/desktop_home_page.dart | 1 + .../desktop/pages/desktop_setting_page.dart | 16 ++++++------ 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6c1245a7d..ba7e3d762 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1735,6 +1735,7 @@ Future updateSystemWindowTheme() async { } } } + /// macOS only /// /// Note: not found a general solution for rust based AVFoundation bingding. @@ -1762,3 +1763,27 @@ Future osxCanRecordAudio() async { Future osxRequestAudio() async { return await kMacOSPermChannel.invokeMethod("requestRecordAudio"); } + +class DraggableNeverScrollableScrollPhysics extends ScrollPhysics { + /// Creates scroll physics that does not let the user scroll. + const DraggableNeverScrollableScrollPhysics({super.parent}); + + @override + DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { + return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor)); + } + + @override + bool shouldAcceptUserOffset(ScrollMetrics position) { + // TODO: find a better solution to check if the offset change is caused by the scrollbar. + // Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity]. + if (position is ScrollPositionWithSingleContext) { + // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member + return position.activity is IdleScrollActivity; + } + return false; + } + + @override + bool get allowImplicitScrolling => false; +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index eee4c6a20..f352c313e 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -120,7 +120,7 @@ class _ConnectionPageState extends State scrollController: _scrollController, child: CustomScrollView( controller: _scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), slivers: [ SliverList( delegate: SliverChildListDelegate([ diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index cde1e6d74..af7f14815 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -75,6 +75,7 @@ class _DesktopHomePageState extends State scrollController: _leftPaneScrollController, child: SingleChildScrollView( controller: _leftPaneScrollController, + physics: DraggableNeverScrollableScrollPhysics(), child: Column( children: [ buildTip(context), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 80dcd80b1..378ddbd1b 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -128,7 +128,7 @@ class _DesktopSettingPageState extends State scrollController: controller, child: PageView( controller: controller, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), children: const [ _General(), _Safety(), @@ -170,7 +170,7 @@ class _DesktopSettingPageState extends State return DesktopScrollWrapper( scrollController: scrollController, child: ListView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: tabs .asMap() @@ -234,7 +234,7 @@ class _GeneralState extends State<_General> { return DesktopScrollWrapper( scrollController: scrollController, child: ListView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: [ theme(), @@ -456,7 +456,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { return DesktopScrollWrapper( scrollController: scrollController, child: SingleChildScrollView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, child: Column( children: [ @@ -908,7 +908,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { scrollController: scrollController, child: ListView( controller: scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), children: [ _lock(locked, 'Unlock Network Settings', () { locked = false; @@ -1094,7 +1094,7 @@ class _DisplayState extends State<_Display> { scrollController: scrollController, child: ListView( controller: scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), children: [ viewStyle(context), scrollStyle(context), @@ -1334,7 +1334,7 @@ class _AccountState extends State<_Account> { return DesktopScrollWrapper( scrollController: scrollController, child: ListView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: [ _Card(title: 'Account', children: [accountAction()]), @@ -1378,7 +1378,7 @@ class _AboutState extends State<_About> { scrollController: scrollController, child: SingleChildScrollView( controller: scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), child: _Card(title: '${translate('About')} RustDesk', children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, From 6f106251f923d215f4b76e93143b1bf50838b141 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 13 Feb 2023 16:40:24 +0800 Subject: [PATCH 047/202] force relay when id is suffixed with "/r" Signed-off-by: 21pages --- flutter/lib/common.dart | 25 ++++++++------- .../lib/desktop/pages/connection_page.dart | 10 ++++-- .../lib/desktop/pages/desktop_home_page.dart | 1 + .../lib/desktop/pages/file_manager_page.dart | 6 ++-- .../desktop/pages/file_manager_tab_page.dart | 12 +++++-- .../lib/desktop/pages/port_forward_page.dart | 6 ++-- .../desktop/pages/port_forward_tab_page.dart | 8 ++++- flutter/lib/desktop/pages/remote_page.dart | 3 ++ .../lib/desktop/pages/remote_tab_page.dart | 2 ++ flutter/lib/models/model.dart | 13 ++++---- flutter/lib/utils/multi_window_manager.dart | 28 +++++++++++----- src/client.rs | 32 +++++++++++-------- src/flutter.rs | 13 ++++---- src/flutter_ffi.rs | 11 +++++-- src/ui/remote.rs | 20 ++++++++---- 15 files changed, 127 insertions(+), 63 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6c1245a7d..ca34eace4 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1405,13 +1405,14 @@ bool callUniLinksUriHandler(Uri uri) { connectMainDesktop(String id, {required bool isFileTransfer, required bool isTcpTunneling, - required bool isRDP}) async { + required bool isRDP, + bool? forceRelay}) async { if (isFileTransfer) { - await rustDeskWinManager.newFileTransfer(id); + await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay); } else if (isTcpTunneling || isRDP) { - await rustDeskWinManager.newPortForward(id, isRDP); + await rustDeskWinManager.newPortForward(id, isRDP, forceRelay: forceRelay); } else { - await rustDeskWinManager.newRemoteDesktop(id); + await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay); } } @@ -1422,7 +1423,8 @@ connectMainDesktop(String id, connect(BuildContext context, String id, {bool isFileTransfer = false, bool isTcpTunneling = false, - bool isRDP = false}) async { + bool isRDP = false, + bool forceRelay = false}) async { if (id == '') return; id = id.replaceAll(' ', ''); assert(!(isFileTransfer && isTcpTunneling && isRDP), @@ -1430,18 +1432,18 @@ connect(BuildContext context, String id, if (isDesktop) { if (desktopType == DesktopType.main) { - await connectMainDesktop( - id, - isFileTransfer: isFileTransfer, - isTcpTunneling: isTcpTunneling, - isRDP: isRDP, - ); + 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, }); } } else { @@ -1735,6 +1737,7 @@ Future updateSystemWindowTheme() async { } } } + /// macOS only /// /// Note: not found a general solution for rust based AVFoundation bingding. diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index eee4c6a20..71660cfa7 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -66,7 +66,8 @@ class _ConnectionPageState extends State _idFocusNode.addListener(() { _idInputFocused.value = _idFocusNode.hasFocus; // select all to faciliate removing text, just following the behavior of address input of chrome - _idController.selection = TextSelection(baseOffset: 0, extentOffset: _idController.value.text.length); + _idController.selection = TextSelection( + baseOffset: 0, extentOffset: _idController.value.text.length); }); windowManager.addListener(this); } @@ -149,8 +150,11 @@ class _ConnectionPageState extends State /// Callback for the connect button. /// Connects to the selected peer. void onConnect({bool isFileTransfer = false}) { - final id = _idController.id; - connect(context, id, isFileTransfer: isFileTransfer); + var id = _idController.id; + var forceRelay = id.endsWith(r'/r'); + if (forceRelay) id = id.substring(0, id.length - 2); + connect(context, id, + isFileTransfer: isFileTransfer, forceRelay: forceRelay); } /// UI for the remote ID TextField. diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index cde1e6d74..ced8e33eb 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -556,6 +556,7 @@ class _DesktopHomePageState extends State isFileTransfer: call.arguments['isFileTransfer'], isTcpTunneling: call.arguments['isTcpTunneling'], isRDP: call.arguments['isRDP'], + forceRelay: call.arguments['forceRelay'], ); } }); diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 27bb0377d..988baca57 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -46,8 +46,10 @@ enum MouseFocusScope { } class FileManagerPage extends StatefulWidget { - const FileManagerPage({Key? key, required this.id}) : super(key: key); + const FileManagerPage({Key? key, required this.id, this.forceRelay}) + : super(key: key); final String id; + final bool? forceRelay; @override State createState() => _FileManagerPageState(); @@ -102,7 +104,7 @@ class _FileManagerPageState extends State void initState() { super.initState(); _ffi = FFI(); - _ffi.start(widget.id, isFileTransfer: true); + _ffi.start(widget.id, isFileTransfer: true, 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 b2566e267..7540f7662 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -41,7 +41,11 @@ class _FileManagerTabPageState extends State { selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, onTabCloseButton: () => () => tabController.closeBy(params['id']), - page: FileManagerPage(key: ValueKey(params['id']), id: params['id']))); + page: FileManagerPage( + key: ValueKey(params['id']), + id: params['id'], + forceRelay: params['forceRelay'], + ))); } @override @@ -64,7 +68,11 @@ class _FileManagerTabPageState extends State { selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, onTabCloseButton: () => tabController.closeBy(id), - page: FileManagerPage(key: ValueKey(id), id: id))); + page: FileManagerPage( + key: ValueKey(id), + id: id, + forceRelay: args['forceRelay'], + ))); } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 2385813eb..2ac6bf23a 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -26,10 +26,12 @@ class _PortForward { } class PortForwardPage extends StatefulWidget { - const PortForwardPage({Key? key, required this.id, required this.isRDP}) + const PortForwardPage( + {Key? key, required this.id, required this.isRDP, this.forceRelay}) : super(key: key); final String id; final bool isRDP; + final bool? forceRelay; @override State createState() => _PortForwardPageState(); @@ -47,7 +49,7 @@ class _PortForwardPageState extends State void initState() { super.initState(); _ffi = FFI(); - _ffi.start(widget.id, isPortForward: true); + _ffi.start(widget.id, isPortForward: true, forceRelay: widget.forceRelay); Get.put(_ffi, tag: 'pf_${widget.id}'); if (!Platform.isLinux) { Wakelock.enable(); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index ca354f297..ee5dd9b53 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -44,6 +44,7 @@ class _PortForwardTabPageState extends State { key: ValueKey(params['id']), id: params['id'], isRDP: isRDP, + forceRelay: params['forceRelay'], ))); } @@ -72,7 +73,12 @@ class _PortForwardTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - page: PortForwardPage(id: id, isRDP: isRDP))); + page: PortForwardPage( + key: ValueKey(args['id']), + id: id, + isRDP: isRDP, + forceRelay: args['forceRelay'], + ))); } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 211d36c39..f9db985d9 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -34,11 +34,13 @@ class RemotePage extends StatefulWidget { required this.id, required this.menubarState, this.switchUuid, + this.forceRelay, }) : super(key: key); final String id; final MenubarState menubarState; final String? switchUuid; + final bool? forceRelay; final SimpleWrapper?> _lastState = SimpleWrapper(null); FFI get ffi => (_lastState.value! as _RemotePageState)._ffi; @@ -107,6 +109,7 @@ class _RemotePageState extends State _ffi.start( widget.id, switchUuid: widget.switchUuid, + forceRelay: widget.forceRelay, ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 9b00b481f..c251aadc1 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -70,6 +70,7 @@ class _ConnectionTabPageState extends State { id: peerId, menubarState: _menubarState, switchUuid: params['switch_uuid'], + forceRelay: params['forceRelay'], ), )); _update_remote_count(); @@ -104,6 +105,7 @@ class _ConnectionTabPageState extends State { id: id, menubarState: _menubarState, switchUuid: switchUuid, + forceRelay: args['forceRelay'], ), )); } else if (call.method == "onDestroy") { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8cf90eba9..d0a2ea601 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1339,7 +1339,8 @@ class FFI { void start(String id, {bool isFileTransfer = false, bool isPortForward = false, - String? switchUuid}) { + String? switchUuid, + bool? forceRelay}) { assert(!(isFileTransfer && isPortForward), 'more than one connect type'); if (isFileTransfer) { connType = ConnType.fileTransfer; @@ -1355,11 +1356,11 @@ class FFI { } // ignore: unused_local_variable final addRes = bind.sessionAddSync( - id: id, - isFileTransfer: isFileTransfer, - isPortForward: isPortForward, - switchUuid: switchUuid ?? "", - ); + id: id, + isFileTransfer: isFileTransfer, + isPortForward: isPortForward, + switchUuid: switchUuid ?? "", + forceRelay: forceRelay ?? false); final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 3af189ef6..864659a66 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -41,11 +41,15 @@ class RustDeskMultiWindowManager { int? _fileTransferWindowId; int? _portForwardWindowId; - Future newRemoteDesktop(String remoteId, - {String? switch_uuid}) async { + Future newRemoteDesktop( + String remoteId, { + String? switch_uuid, + bool? forceRelay, + }) async { var params = { "type": WindowType.RemoteDesktop.index, "id": remoteId, + "forceRelay": forceRelay }; if (switch_uuid != null) { params['switch_uuid'] = switch_uuid; @@ -78,9 +82,12 @@ class RustDeskMultiWindowManager { } } - Future newFileTransfer(String remoteId) async { - final msg = - jsonEncode({"type": WindowType.FileTransfer.index, "id": remoteId}); + Future newFileTransfer(String remoteId, {bool? forceRelay}) async { + var msg = jsonEncode({ + "type": WindowType.FileTransfer.index, + "id": remoteId, + "forceRelay": forceRelay, + }); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); @@ -107,9 +114,14 @@ class RustDeskMultiWindowManager { } } - Future newPortForward(String remoteId, bool isRDP) async { - final msg = jsonEncode( - {"type": WindowType.PortForward.index, "id": remoteId, "isRDP": isRDP}); + Future newPortForward(String remoteId, bool isRDP, + {bool? forceRelay}) async { + final msg = jsonEncode({ + "type": WindowType.PortForward.index, + "id": remoteId, + "isRDP": isRDP, + "forceRelay": forceRelay, + }); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); diff --git a/src/client.rs b/src/client.rs index a21592578..05b34d781 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,15 +3,15 @@ use std::{ net::SocketAddr, ops::{Deref, Not}, str::FromStr, - sync::{Arc, atomic::AtomicBool, mpsc, Mutex, RwLock}, + sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, }; pub use async_trait::async_trait; use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "linux")))] use cpal::{ - Device, - Host, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait}, + traits::{DeviceTrait, HostTrait, StreamTrait}, + Device, Host, StreamConfig, }; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; @@ -19,26 +19,26 @@ use uuid::Uuid; pub use file_trait::FileManager; use hbb_common::{ - AddrMangle, allow_err, anyhow::{anyhow, Context}, bail, config::{ - Config, CONNECT_TIMEOUT, PeerConfig, PeerInfoSerde, READ_TIMEOUT, RELAY_PORT, + Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_TIMEOUT, - }, get_version_number, - log, - message_proto::{*, option_message::BoolOption}, + }, + get_version_number, log, + message_proto::{option_message::BoolOption, *}, protobuf::Message as _, rand, rendezvous_proto::*, - ResultType, socket_client, sodiumoxide::crypto::{box_, secretbox, sign}, - Stream, timeout, tokio::time::Duration, + timeout, + tokio::time::Duration, + AddrMangle, ResultType, Stream, }; -pub use helper::*; pub use helper::LatencyController; +pub use helper::*; use scrap::{ codec::{Decoder, DecoderCfg}, record::{Recorder, RecorderContext}, @@ -943,7 +943,13 @@ impl LoginConfigHandler { /// /// * `id` - id of peer /// * `conn_type` - Connection type enum. - pub fn initialize(&mut self, id: String, conn_type: ConnType, switch_uuid: Option) { + pub fn initialize( + &mut self, + id: String, + conn_type: ConnType, + switch_uuid: Option, + force_relay: bool, + ) { self.id = id; self.conn_type = conn_type; let config = self.load_config(); @@ -952,7 +958,7 @@ impl LoginConfigHandler { self.session_id = rand::random(); self.supported_encoding = None; self.restarting_remote_device = false; - self.force_relay = !self.get_option("force-always-relay").is_empty(); + self.force_relay = !self.get_option("force-always-relay").is_empty() || force_relay; self.direct = None; self.received = false; self.switch_uuid = switch_uuid; diff --git a/src/flutter.rs b/src/flutter.rs index a60e379f9..0161e644a 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -3,19 +3,19 @@ use crate::{ flutter_ffi::EventToUI, ui_session_interface::{io_loop, InvokeUiSession, Session}, }; -use flutter_rust_bridge::{StreamSink}; +use flutter_rust_bridge::StreamSink; use hbb_common::{ bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, ResultType, }; use serde_json::json; +use std::sync::atomic::{AtomicBool, Ordering}; use std::{ collections::HashMap, ffi::CString, os::raw::{c_char, c_int}, sync::{Arc, RwLock}, }; -use std::sync::atomic::{AtomicBool, Ordering}; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_CM: &str = "cm"; @@ -114,7 +114,7 @@ pub struct FlutterHandler { // SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`. // We must check the `rgba_valid` before reading [rgba]. pub rgba: Arc>>, - pub rgba_valid: Arc + pub rgba_valid: Arc, } impl FlutterHandler { @@ -449,6 +449,7 @@ pub fn session_add( is_file_transfer: bool, is_port_forward: bool, switch_uuid: &str, + force_relay: bool, ) -> ResultType<()> { let session_id = get_session_id(id.to_owned()); LocalConfig::set_remote_id(&session_id); @@ -477,7 +478,7 @@ pub fn session_add( .lc .write() .unwrap() - .initialize(session_id, conn_type, switch_uuid); + .initialize(session_id, conn_type, switch_uuid, force_relay); if let Some(same_id_session) = SESSIONS.write().unwrap().insert(id.to_owned(), session) { same_id_session.close(); @@ -667,7 +668,7 @@ pub fn session_get_rgba_size(id: *const char) -> usize { let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; if let Ok(id) = id.to_str() { if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { - return session.rgba.read().unwrap().len(); + return session.rgba.read().unwrap().len(); } } 0 @@ -692,4 +693,4 @@ pub fn session_next_rgba(id: *const char) { return session.next_rgba(); } } -} \ No newline at end of file +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b4e79b361..3025d722c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,3 +1,4 @@ +use crate::ui_session_interface::InvokeUiSession; use crate::{ client::file_trait::FileManager, common::make_fd_to_json, @@ -20,7 +21,6 @@ use std::{ os::raw::c_char, str::FromStr, }; -use crate::ui_session_interface::InvokeUiSession; // use crate::hbbs_http::account::AuthResult; @@ -84,8 +84,15 @@ pub fn session_add_sync( is_file_transfer: bool, is_port_forward: bool, switch_uuid: String, + force_relay: bool, ) -> SyncReturn { - if let Err(e) = session_add(&id, is_file_transfer, is_port_forward, &switch_uuid) { + if let Err(e) = session_add( + &id, + is_file_transfer, + is_port_forward, + &switch_uuid, + force_relay, + ) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index e44e31401..447c2e31d 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,24 +1,24 @@ +use std::sync::RwLock; use std::{ collections::HashMap, ops::{Deref, DerefMut}, sync::{Arc, Mutex}, }; -use std::sync::RwLock; use sciter::{ dom::{ - Element, - event::{BEHAVIOR_EVENTS, EVENT_GROUPS, EventReason, PHASE_MASK}, HELEMENT, + event::{EventReason, BEHAVIOR_EVENTS, EVENT_GROUPS, PHASE_MASK}, + Element, HELEMENT, }, make_args, + video::{video_destination, AssetPtr, COLOR_SPACE}, Value, - video::{AssetPtr, COLOR_SPACE, video_destination}, }; +use hbb_common::tokio::io::AsyncReadExt; use hbb_common::{ allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType, }; -use hbb_common::tokio::io::AsyncReadExt; use crate::{ client::*, @@ -286,7 +286,9 @@ impl InvokeUiSession for SciterHandler { } /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. - fn get_rgba(&self) -> *const u8 { std::ptr::null() } + fn get_rgba(&self) -> *const u8 { + std::ptr::null() + } fn next_rgba(&mut self) {} } @@ -467,7 +469,11 @@ impl SciterSession { ConnType::DEFAULT_CONN }; - session.lc.write().unwrap().initialize(id, conn_type, None); + session + .lc + .write() + .unwrap() + .initialize(id, conn_type, None, false); Self(session) } From 201646da4c0248bdc64dffac22c243489abfaa51 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Mon, 13 Feb 2023 18:20:40 +0900 Subject: [PATCH 048/202] add translate ja readme --- docs/README-JP.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/README-JP.md b/docs/README-JP.md index 6d3b6d380..36c74dfed 100644 --- a/docs/README-JP.md +++ b/docs/README-JP.md @@ -14,7 +14,7 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます。](https://github.com/rustdesk/rustdesk-server-demo). +Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます](https://github.com/rustdesk/rustdesk-server-demo)。 ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) @@ -58,7 +58,7 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CON -## [Build](https://rustdesk.com/docs/en/dev/build/) +## [ビルド](https://rustdesk.com/docs/en/dev/build/) ## Linuxでのビルド手順 @@ -105,7 +105,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ cd ``` -### Build +### ビルド ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh @@ -154,7 +154,7 @@ target/release/rustdesk これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install` や `run` などの他の cargo サブコマンドは、ホストではなくコンテナ内にプログラムをインストールまたは実行するため、現在この方法ではサポートされていないことに注意してください。 -## File Structure +## ファイル構造 - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数、その他のユーティリティ関数 - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ @@ -165,7 +165,7 @@ target/release/rustdesk - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ。 - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード -## Snapshot +## スナップショット ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) From 65e1b7d74e653319fc453f24836c14b88e824a60 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Mon, 13 Feb 2023 10:53:05 +0100 Subject: [PATCH 049/202] edited icon #2722 --- .../AppIcon.appiconset/app_icon_1024.png | Bin 53345 -> 37517 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 5475 -> 3032 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 978 -> 448 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 10828 -> 6198 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1555 -> 875 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 23370 -> 13870 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 2851 -> 1583 bytes res/mac-icon.png | Bin 51695 -> 37517 bytes 8 files changed, 0 insertions(+), 0 deletions(-) diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 9af6f2121eb5a5394d671f8e0ab600cef240b3cb..fc39cb2ff2713d9b7e7c93bc6091721947d5c893 100644 GIT binary patch literal 37517 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 a2it#tsAIaS9p3lv(b>V(zQoQq_WuBm=|rLc literal 53345 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

^0>;>{-A-^Ls1s>XX&4yG45lUQ|yu+oo`W-dQ3 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 diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 4bed6f3fa9928528fa6e0046af8bc011861b76f3..3bd2b7ede030927dcf652ee5db6131bc94df7c69 100644 GIT binary patch delta 433 zcmV;i0Z#tX2fzc68Gi!+001a04^sdD0J>02R7C&)000000RaI300960|NsC0`T6649sARW5($XWmZ0HjGoK~xyi zZO%mk!cYuF(SITpoYdW2acBSAB27EwjdIVQeCE&CIgFdL)Od-Bs%9P@TLgTUpGIqT z9}!?VVHoVPel?X@07?(iP01y|D!w9tlPcIK?=Bz)G(go3a15^0l3^qS z+QkM7JFg!e0cc?BVWcRf3_Wlt=Rl9sJ2r=~27trK^(q|;j}3l(#;;)9f}b(H?1d>! b*kS(wqNfsT9EuRY00000NkvXXu0mjf_j}yV delta 967 zcmV;&133J^1JVbO8Gi-<001BJ|6u?C0fcEoLr_UWLm+T+Z)Rz1WdHzpoPCi!NW(xF zhTo=2MJgTaAmWgrI$01Eanx2QLWNK(wCZ4T=^r#{NK#xJ1=oUuKZ{id7iV1^Tm?b! z2gKFINzp}0{4Oc9i1Ci&9^U)jm%Hx(p;={`)iVKTx@~4s34bw{Ull{I5D-F`W<+L| zF)vAJ_>Ql81o(az=UM)Be~tmQU@;&d63;Qiw23!}XEtqv^FDEum1ULqoOs-%3lcwa zUGeyhbIE0aXGYCzCP^G67E4{MbTKQN8u2u7Ox1MC7jhn}oVPe@)jDh6lfN)r&{vkZ zPHO}yEMf@~M1Lr#qk<}I#A(+_v5=wjgpYsN^-JVZ$W;L&#{z25AiI9>Klt5St2j03 zC500}?~CJni~^xupw)1k?_D?aC4=ki2wis2}wjjRCocUlh11tK@`WoGy9{dra_X{Lam@_E@_D%cnOLuJ=PvA zIf{7l*rWafDn0aWDHHqU&M1E|H;@*PI0A zCM~`BxfvjzH1PQAAr687EBSPs6Jvk0B`TjJU~sJ6PGJ6Wx8gJbw`d+uzO-PHoM$;P z_!qYJB|4`ZsU$FWr8}@_%@AZn8aN_3dbs@nNb1PrVE#aExUC+aQ{yrW`T^I*DF`?Y zkAFyWo!TDz6YzS^NAsAG1OtVX1-BoNKF4tXI>*EXhWx0KBrtQ4K~l9>yB$1u*9HVf z>8eGZ-~%1#rk>wbgJpR1M&Rj(0Li3)GzD59KiCYpQ47mA&iASc0m`1e?S5}CEvMO} zz@|e(WVR_2%Z`n)L|8q_(E#ObWzWcst3V43OLsUn_rswT#u+lh-Rn`UR^O|f7@$0@ pynWVXa=Wj8zf2KCc^m(@egUUy%9+Dj8;bw{002ovPDHLkV1fvh%1i(N diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index 22893b8ea78c8c2d4dc1835a5e19b3c056e11d28..88f2eee49a9a8162eeedf5df764e8842ac2d0912 100644 GIT binary patch literal 6198 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 literal 10828 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 WX8Oy%I)Y}20+i&`WNY5Q!u}tVQGC(> diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 583a485712bcc8691458c7abb38b37906ae6649a..18151e82b11bfd52df745f84cdc3fb0fddc45f30 100644 GIT binary patch delta 863 zcmV-l1EBnq4C@Av8Gi!+002a!ipBr{0b)>0R7C&)000002L}fN0s;U400000_xJbf z>+Amh{{R2~;o;%_{{HXp@B91vM@L5j0s;a80ssI21Ox;C00960|NHy<|NsB7v9Y3} zqR!6F{QUg?|NpPAuk`fv|NsC0|Nj7g`v8*j0J7%*cm4o+{eJ*``~ZUb0I%o(wC3;k z|KRZd0EPJghxh=D_5g|Y0G9CpneYIh?f|9i0I2H#s_Fo(=>WIo0GsfDz5d|!`~Z&h z5Tx(`p6>vo?Etyt0KDVB-2d%LqNkiOPc@y0hvieK~y-)t&w$e!axwkX_{cgp)P18DOLqaixhV$ zQg?U#{=K~r?#T4dzM0J2?fXq`_pRceG^bJ9Wm0RV`?hV&{-;K4JwQWhxkxcd5u!gO z-OH`)qJL{?bN@3VRc0}u7ccp@)wT5vAY$>a-f#5av!nQ2i>~YtBGc=XMF5A7r@_#6 zpE!7y-e3^Gz(HsSfQYv4Zo666BdVNXU`PV4oX$WndSS#~dh^WxO6C{=+O%@Q*pn(sRX_lJe*o_FD_}7s7D+MInZcmu_DZH059rckLd@)M? z67S`Oj#~7>j(W9JYCgbTLecyn06yvi62p55*2p%MaKe2MqJ55QN&q;W2=Ir5xDmvf zj#xneJ0rc3I4Jp!6AW(6rEEwPn6hTX6;1pPld0eZMyGreZ30lPB ptY+h|Oh%K1Q4q1rkW36KI=}E|c%z@Oh>`#R002ovPDHLkV1jA>&Itej delta 1549 zcmV+o2J-pq29pes8Gi-<0047(dh`GQ0fcEoLr_UWLm+T+Z)Rz1WdHzpoPCi!NW(xF zhTo=2MJgTaAmWgrI$01Eanx2QLWNK(wCZ4T=^r#{NK#xJ1=oUuKZ{id7iV1^Tm?b! z2gKFINzp}0{4Oc9i1Ci&9^U)jm%Hx(p;={`)iVKTx@~4s34bw{Ull{I5D-F`W<+L| zF)vAJ_>Ql81o(az=UM)Be~tmQU@;&d63;Qiw23!}XEtqv^FDEum1ULqoOs-%3lcwa zUGeyhbIE0aXGYCzCP^G67E4{MbTKQN8u2u7Ox1MC7jhn}oVPe@)jDh6lfN)r&{vkZ zPHO}yEMf@~M1Lr#qk<}I#A(+_v5=wjgpYsN^-JVZ$W;L&#{z25AiI9>Klt5St2j03 zC500}?~CJni~^xupw)1k?_D?aC4=ki2wiuN=ZaPRCodHS4~V@RTTd2oBzR5rsZdfLqNcVRbz;zsUk70E^Jz$ z3w9Gt>QCGN(VDc-sBwWoNE1opswu8rnGuu5R%4@Btt1sqG`Q$gr%Wi;;!I(N;myyx z-ZKNkynmT{%d_!s;Ynul-hKC-@4M%obME)-_?ON8KLOcbV`F2(@bK`lo}Qk=O-)VB zj4|7tIOm+_qS0vNLt&$%qhkvT3%_njpsTBEubBUl&*!VX6VOs4;$i`<{r&x~1cSk6 zdV71jr>CbcDGAWZw*uDtQL*D zSF)18+cO#bo`fmvJD(eK0@P>6^ysnSSO%o zIo|tTB%HAcwA*Ksg3E03S_%{4WRd)tXc8l>+u@XysFc9PC`UZWOsgN+>#_t+R`1kI zsrAGpqsC>K!1ZPEmw($> zfhMvY#e}nB6zW!w9X}>A#j(XTLpoQUguIl1C>{B`x3zrrl=$FWz5pqKT~*AqhgX(3 zGC2;1#VDl2-0(87-kk53v}PrNjz%;4L<%^6mE+*O)(C{IrJ<)`N@6;!EEDJy&C=&$ zB6~N-Vks`0T45`q8NGga%@n~Jhku60cm4?iE>UjJKLVWmqHurW35MW<+ScIWFzWkc z72&uDe@V?BX>r4)*gj<^JiZV3{3=8D{S2?|XQI6pI^3CA19ervfi^8qlx$V3Iuy?0 zjTs%`M9vg?pXdogyS+-%N&>Ya5oZQB?+N_nvj$FmW8{xyw=0W#c2J}_et%9FNOPm& zsHUsak5-}Ht&X0F0BJat{yTP-3^oCrb0>rLOPEN_4=$#7uMayaIzbC>wq|{A`G(&9E(Yw z-)O&R)g8MSIs>ejm@0000>yjYwfeodG>94yZth%zxK9{R@)*Li^b#d+nkS&4}u^VhPSzZfPj#Y5P?A07Po2J%JskW ze*xPfo6W{xFx#eWj>F;p?_gW`Kj{BeZu70Jt!=)zxVVkkrj3n_rKP2958L?p`T6PT zX%>ss($ccJx;j2Se*gY`Cnu-vh}%v|N=o|s`?vAi9)^d9w*lJP+S~e?nwo9RmoHzo zP21$-PH!otF~er|4VJB{tEw!zyuY=;108=!OyM683?qky^w5|+T5Kj8KM0TLeLy;Nb%BV}a;ZkUb4DXF=fv$omU!u7a{LP%{il#zDv? z$eIB~6QFzqRE~iBzo23S&=$dyY4B(sJe~oG|3LCQxU~Y(=fLwx5WWsYuY=qv@OBu) zFN1eOAY~r-utD(zs2u{qTOe{B&}YEgq1iPSKsT84EO2KT1hVKOOCWX?__Cq0apv+S z2;ZFDV4<~Jpn8Nk!$yBBfLm*Ilk3cXth(PUdIuYQ%K~vLSl#(sl;G);}dB!g``SDWM7#qo1r?f3V>GNKdTXg0M(lJS?Sm%E=**&$zpFPk2 zY=*yngnWxl3TN}jucK{iNc}AN-n{k?IAlcQdmq?Ac{DCo!I}M7FDB99a2%r6^ZyWQ9S=SG_>3J^r^hc8^+~HpHCkwc6K7Lh~+sc$^Xu6 z8Tj1xC~+QCDRTA*GOpG;a<#prh z#M7QSZIcRWzY9BSbBFY$u)~DPJJbK^l!gbfuKQ1m#h8iRYABs$d{?{@?IcHi@|@FI z-;TB+ljHME&LJ(@V6ARfZ`@;^x*b0}pw@`afu<8osS(q^d2C_&W*A@Oom}=AJ8v$h z2N7_ioMDjYr+G=@TUJX-e!(ki1>IdC*c8??p68v70seK183ktRZRd_^<2NPmf1p3X zs7;tkJS7uLw=R?)9D(O&Esi*N-45(INw8V^g);-1g!7IG<018^LPaGn1~w8hj7 zEY20)8||yrtG?Sw^O#OQGJ$TG{q-uC(a{k?8rMhU^*2cVXSAevDY}1b+4IB~! zj$m?x)xh!0*OLf_%62Zk-Fku-I!N*3L{hm8fBt1v1^B+MW3(i7iR7Pebl0CO{^me^ zi_JN{T4Czf{ib$yI#n`t%1GIZvrdV?IP&y|hpYDQ+jV!bW8&{H^vzW1uxotYx=gGE zQ-}xgeX2z=<`Fm=E$|-`SMbYsHsqNj@0>1h20Y!oqvMTs@wX3Y3nYxa1mDvCdS7lD zK)Kj>Cnq^cjJ1U$M@6JHqu&&BBJF3*=GtEAx;k#Je#qzU;e^g;4x0zvC{~bXBzv$8 z^9Xu`+snqQT|_VHgYF+gxcWzu^IgGbpl^k&o#cCXGP@EZ(#-&Wu7oh&M|$FH%yB53 zZ;G)S%!kxD4JyDk@yrR#Zp@?Ch2Q8TDC?YJ?Dx&fxd3UX0P=g z%KzdHRJ~?0A*5Rh#V1_}c1_xkwd5xW4|CF8e&$`G1KAnC6E4Xe2xC+qw%XIg$hX0P z*ch~9zd`u>UoZ_)VdJq~z#d5uv$3~C9MM_ox{0a~=fvJizr;zQgevhlM+^qldk4?2 zgv(KWDv{kH>fc`jh7=vLXcjaQA#Ps`7Nh2Nvxzjp1R(=fV`Jn~=tX7yz)`$==WgIw zXq|a~;cQd@*5;F|^)0_mrEB;)T{MqD^>^94RSLKKnIL5R3t zmhvw5i4x#$ouevFn=H%{Q2W6He5-ep#s;9HYMrrF)g4LNzwqSjL7eUnSK!QWE6^6G z3P3a5baiL7Q9g`4Bo+xhA`iKtqd#HLkxL68*f^x&-S2A0RzlgA&?pdJSwES8e@fAB z(t&ftK1d;Jr`y{(Dlm^$T^J!8-x;7CltsaL$M`-(WT1I{E0s zOedHET=}NvGN=+p3HoQOKd6XzaTB)^Wyy@I-R5K08G0XiY3F|=XtY*(>N)Fr>!Wi% zkS_h;B_uTQG5FMBFx*-q^B)<7F2(Pr@yTM0lPMk77>dK~XC^yG%3H`prrt6QpN0wr z3wa=%(#AGjLkEmktCf8O7AY1C7oiU)&pX75$-O73U}+2yMN`!)QsD|4NQ?{U3+`yOn6Ar3y!%j6(BW;yEYNxcqz#YH&@vC z0E2fms#nEvBJIDKymD#jcQLXWzfP~-d&h^oU4%~du7TqrQBk6qO6Kio9dc;re?0CS zez4T8dEe?*5M3nbR@qb)#*n$f8_4-O8si3Immy(``1!z43-t9<==Q%co?cO}7x0&_ zu8afEl^C-a)kpb5u_gq{6z*c=dJi0s$vuuaWk>K(f;!B~q`g=RBL-b4R_-FY%5nd1 zJBUukc&R!i>&#f&5F?;gp6GFl60To{`3`nWG`wzcUGXWi@VC+ zd1v7MU+CXR!R+GV?6dR>XHaVpfx7V;h&D3g#c7PWr@%6dK~IOoBBD>DKd&A(BDybc z5JjNWu9qn&G!q5{nRlb0i}(`!M{3Gb1y{= z>SC>36X`DWJ|s#F7uX5OBetRkM929iPUAX%)=CYFdnG1fqV3{NFX-`+bU+Al*)R+X z>sDfUqfThDBr$HwxEIYOxPgF}*|KuRyO4Wm8s;iiBC?8TfHz$8U#{ptt5AqT3E6#| zJk|kN#&q78$mQvsPIz6y58u0x_LWNxAV!jOsnD!*rqol0d;tj3`IXfwe+eX-CQNKU6dnrTe_um=g1M&C-H!CsbZLlVxNaG0_wK7-GB*nv6vVlVmQ zGdRQf{9lZ{C@)@F8+~C$j<2hMyl>)5Q|X`WXdfPD31Xu_1d}+9ei;0T8q{$r{NXOO zi@wng@R#J?FPQHA7}mdnaX_Eap@ax+4}IXUm%n=WnV_Ycp_xd;4c^L$I+@WGDeMwu zGbFJkCmjqKS=g-oqK~hkX?YE|-pf=IvF@McKvP|11FDsWGEOnn!4A65p1|^>)cW<0 zGS$nMWpH}><(Xi_Eqqz79n72}Pru`<8ojC-sy=cJ>AZ&e`E*AQnj&N2(@zm0rh)RM z;{4<*AVMzsYv4X|`9sW6Vf-IG^Vv6inzF2Q91^($8`$^d{2s(t&iB}j04G*cEHod} z>AxjTuBzk`ikc75rL=znQF#sD#;|{7A`H4;goz9Y43wNgOFl_oLo&ZHwU3`4Vcc;?JQ;f=fk@%f$ns{ z#HD_(SsvpN^sL`M=1#2XRA27a4bluw3iD9tsVz8xGY*Kn0DTs~2O23p8up7+dN0#Z zv$z7X@7mcnZz#*9V$@WSg&D8sd4h6W|6`n{;su!(gno;WUy6`7p8Pqc89Ap8L;b>> zb3-R?0)I@?s-Uc57`o`#OIy{?0DVKdFQV?evA0Wbz31pBFgKRVQM36Nufio8S|42W z(>xHbiTg9drQq9vX}=xulw)o|MD7uWWTJ0vP}BJ4D!1lNmNKy7Fu-1J_78{`L+56> zaxx9;qdU;l51&5m+?9LOWAEcGjpT#7j*RXq6r9E-rKQ(XxYh*UB zs;TohCT(0}x2@+D)O&ts3z$~LDvN>;-ckv)d5WjAy*QHl)lP=;Z>1MM8M@5|Ef z-%Ili#DXW^ddMco8!&eRIl7wURNacIY)-cwJLT01eV&&QA6(5E*gAacF%OE(Q6$sN zQP24%fBd55?&Ih&d@sj+U*jz{mt1_CxV=ThL~`RVirM62$p|mLHzli?*_l{z#EN6p ze;BWW9FoyoTEi5$Ek9Hw*tSPvBVw*<*c!t@_UNr~NO};N!dnL66N29DHQbX7eOtVD zLQHesW|D6+v%**>XY${Ik;*~FK3cBWGi_#h|84tI-m9tb&D_HnC6TNHTTnvX3w`TT zVbvx0Y;czLXZYgg-sANA6Bs3}&Hm=1(UTiy0-IY#b$j&SvZ=4(Mz5_-qn_B`;*1-} z=W}38Lhi8+#b9;gp#%Y4 zl7?p&8?fn|Z3)p52s$OMaFp6+OREtPE7WGn_>GlKT9TsAD!g#1TA1K$v@xK$>d8l9 zT`tHL5Y1XN>!a}3N%F+oO*bvg>>Z2+-ZFWF?#Bkqn37 z#KC7>k5bQZ?O!uO^}t)ss!NeOwU0Afq-gZ7=ZABS%5o{66?$vb4JAOH{g~R@%4}Nj zl!Qw`EG`LyRAH1cnx`P74QBjvi)D{TWPd$BJLg>ti`a95XaQ6VDKq1v@w3V0ryp4x z(+hA(-aBjn&;_<6WqgfRdae&j7u_9ii(!$Y-oOV2&89~R&)N3|s> z*9}18`8ZVgw~kp!#4T)C#GeZj(vy${F%c$joKQ;^=w7?QEzh*E7p5(2NjYCEy6VH_ zN<}V=fzX2t`gisrrKPvEBmkLDI)DFT|cxwJAdKH;v6*U=!fuo6b~bT8k+3&9q19Vxysd^lul?n=W4F1|Jnp- z*e*h0#DL%-4JM6b6eHzzr;z+VK$H0Cp!;(>Jg$PVR+MN+y=Z&n>lsJa4oA_$60LTzt zo>!$*o2}T|TVpia`!r~M+ED>fvRbtH|IoYWj3kA~fC70178_q)X-~z#=g5N&Uiqgh ziKXYj?<^({Kg66`i86pLlD{? zHIP9n7;=VBe;YF|MAHF=p+loz?YYH~h_Q#qzaSN|b2okhdoO7(jODmU*qHkxaD%6L zHze85I1qLo8%qRWaOFC>tyDfwvcxeQK@O*a0_-LPdU<~s|8$lm7vUY3z15&<*s}q1 zt=RxZ_s>Yl@2h-YK~;Jo>-G8Q<637Lnz+WQdp{qwTv(8ag9@*0LR)vLJ@$zp~c? z!P?P+ND*W!MG>h?(mqZNxr*AgW@A1gM>n|k91-R~DpV%Sjuo@u;A+?bVu2Qf?q2VjW zNGCV5X(5~_*Da7E;X4XE@N`2n3r1@9X;czc4XL_-dY_k# z2RHOrx!!w3d#20tBis-txRy_ivgUg!m#$i%A=31uLySYDfggSec1e^Bv~Eq^y}|8k@h!bs;|bMl?YrbIBL;m1z3ahgb3=wW%OvbWx*xlD)dXW z5sFo0p3MR7yRfW(sQPD~bJAo_PIqf)P1%z;7Mvi|} zhcNsCE$M~kDo))==>MajYytyggCY)PT|NV@taHiGi<5;{<3X^1kRjpw4yg(veNa$g zkvaud|J#F22?F;~E#mj$#u%;;G(x7C7xnq}Mow|Tm;u0prI?ym(|4Z44F$?wPqI90 zD|lvy;NYs(<_^LgOKf3oMUtgQS+=oFw$|&utJS$G7hXNP`V4=$;Kad0bFxAn7#j&v zkbRu+`+PCMry3@&0xorRX1?*6&HS$^WPSe-!N8O?7SZ`GV9eV@9o|T zM$g>mAnlJjFCxuYCL`lMyP;sdQUl)L&&99My3+N_d}2Cs6mh4&{`rUaWj?d;L}o5pBRMx>B4nIJMWjDdi$BH%0c67mO@q zqEcN#r1!n?5acJz+iU(Hl!gkOD2X?U#sY0fLt;B%Y6R`oev z0eYPbGYO;Hhe0_{A+o(VP6sS^i433tq5se&9{yxWrphh?S(u-Cu7HaK{q~{yVUcf` z>Jg99BR=XBF7(QE%wNdB>9wKwDO2F`YqUj<;{l;+9NY%oN21R1A;o;4(=(A+Y9Q*R ziE(r&pkpl!guG; zvu~vs;liQb=^AqMFP|}0FCupiK2!rYzuhwy=M13!Ua|j+M`w0`%=hTRI}moMnG?0u zMiO~IBzB5#fNF zA4B0LV|bMMrdAF_?xi33gtKmjLt$Hu=&J892?bE@oSnTs5AUmR$r+X8r|Y{eK^-T0 z@nGp>V_5*|P!v(cC6p_P#8oRcZ5skDN`HZZ) zWGH;T1;f1zHN_)6rwJkHWZ4c~F=Od{LJH_V4@TgTd*5OAoxS<17d$jYV5TD%T4%NN zZ}p52^g$J}8hHcad^nB!j>!V{=+H8hsmo>QOZ`Dn9Xka!(cEMcG#ELs06x^1HLaLm z-y;v*5G7(ruLNJ8!niGbDo~OD`v&5$;}0kfiI`QEB?Iw!^#Dx5KhqT4U*Z`e%tsj8 zYaKoOw0mvP(6`vP{6kXk%pL-WjTpuE`)=P^T>A<(n+j#T;&53z*pl?m|R{{jb7W zIYN?P`6~?z@xX!S1RVt#G zBNxw@#DyW1bKuBlHDm+C%aRk5%_NF}(*h7P2j>DJ=cVnL{TPw0g^z#GYttuI6~uPM zt1!K5L^OvXvN0ouvHVCZ?h>Eab)_YRBatHFn(^FoRjyoB$Gkxu*Pjb;Gxppd*W46z z#np8Wfh656cmN47hPH8nK<^rm*m9Mp-&6=eAAv_E6n=lG5Jkeqgc;;#bMwN7%;!40zYN`M$({Y zPEWr%c98@%No04`o2S0zDh22Xvm}hotuGWmg+C&gPXG%Q=t_I>tNl5LM{~1-0r!!Z zMt#dFDUQ<%t&a=1VtX3*LH_*Y42Ts0te>qze9gOv_CerPTm7!94RDe)?Mo1fFjQd! z4v_J&%sCKMCCv5Xe_gjwoPdu?l*WTpH*f`vIv>aWInSYt=V0#Pcmj=cG(XEXV}DBd zDMC&^Ub-7ZWIZP#M>BXHMII^ySlQ#tj1$$FOTauP>-Z} z%<1)V=%829k*`O0J%38lE7E6Hc+9sOK9ccJvCULoZQLF}p`Y%WdHua;q45q(|J zlYwR!66k6%jM&GweIT$}X$@LBF;0-z^&WK`RH1JvQh$_)hRBjA&-d6cXyB_(??s7XI}U2{EX0E3{vVB4VqX?de*n1T3|xdoXct9N z@Vsg6`ww7gxL(D2t5VTh$o3}Zy?TL?28On{)8gAP>Tzj&>F3k;Es?TB*$rOiq!@87 ztgsZdG%~a1V3rj`xQUKyr{g)O=P8Arc9>#7mnQqFmT~>L4IG6a<7=4BFEfQl!)^mJ zDmS_?GJe&D&U5&hAzH^-+6L+JuyB&P^B0U-R?zS->SRGL5dosk_QxK>JY{>isSb$x z5`a>bYWI2}FhiYSFKE*XWxN%!D?|-(qUJCZw9{rt3owc?1vywU{KgRbzzPYlAU7fW zkllg3h!^P>r%n;O$0=Nn94sI=4O zA9MRp1%{3zLDsIQ!`5G4au#^T(Y%-Z5OTXAL#ASAI4sdXSf6L%8GB2E@7U5*l*iDW*TV0AC9 z2b_?Xzu73;>rdAk2kKtUxj_A#A=Q%fu3W!qwXa3({O9^b9khW>^7^Yy^qx?0*v9 z&m2IjxhQMHHyAqe@|E51FIxr@ z)w}In5$|KVxe!x`=})xW>f05B8i| z=X3cH^RPz{0W{xBQSr~X(VG~Ctp-0S7Pg&?7=9Io-EKrJGP%uTPA%vD@W=Yv(J-J4 z6TJ)AqCG3wk=Usf4x0+$nVnb>*@p(UP`Lx`2GG*P-Mzvb(;p1=Ib-zzSHRKT1-}-y z>jN3JLOZG?l?1y}*6(F0A32gd%iAFTnbA{scI!6k#79mYL-*M19OH{}7q>swI2n=u z_SD9OdBJXXhc$5 zU$Qo`eGq2E&fau;uZE2|70co+#iQwXO z2dvAd8O$P)1o3zPTRrNX<3WNDX&HR|ktJ>GTui_p$*2i()0y0TG!_lJ3{RQRWqBAr z;KXh^q&kc#lar&%gYxRlpWb0L;%`H)uJv@7=V5bZVZLnsqXwuBpM^BFwf?c7+O29N z`!1SU!1bzaFw-DB7`X9p|1$BO9*8Z?_dIvFP^~9&xwTOvkhEt|dZ+Lc6I21B&jUvo z9(B~Wk`Mu^z*zmH=TAdMOzSKcQHFL23gOdC#SP68B$eQ=0XNyhGn=Q~I2#S_ev8#U z*q*lIl~cN9^UYnNZFa{zAH4tIGa@smQT6-W!LN4HQ?^f+&!wg4c(mP=$+hG;xbXCZ zvgy&dM-4p5SwGLe8_o>s`j_SNYbEH&K9LB<#?P^ppe#O0Lh(Cz_4;mfr@9@!4_$b| zp}flz6r-xF&9HD+-1z>;d-Y;sn4+@l&;x#Ku%YPAwg&v-F%U^*%2B;&5KXj3Ez~6h{8Zb^^8^ ztu8{33Anvqk8-tszP&E_IS94 z*Q|Okogs^wU4rMf8+yB50)s4%)WUL9(&S>tB zG6DnASr3TG7k&etp~0Rw<4>{J>360=;#k_RTVsuD^(%Z&(TQs~$8nXHr!~4O9`c+D zjW96IsL(+D`i2>uOSK_4uehm`f1o$mo~7bVT#n~em=mt0`2@mz>t{ZEE*Hzp{%qXOsk!i>xksh6#k+9uR$PfiUya%BpGFqh zyUfRN=alI?%8WKW1d7TsEr5aG3GBu2jAaMwNWo8Ukpq?by)!3}J(o==B5JR$FR6+^ zr9F{GX32TuwTL1!J^Mz{jMwpLT-Ks8b7$`;iIZ~sYN=ETbo$rPlNTd}G!J(Wx_u(A z3JS`dIUVU|J#81w0cj?~5~BQ45-9s{^gT>L8pZiIAK4RdB5I^6E!?trWgoW?+?!}Z zETV%aKY&|*CG>C~r?N*E;dRtbt1j#qJh-!dpG9i@9z7g64QjuMHs!`cdUTN-)Z;8x z_2IrSsz5NLDpAA*Sr&7eVtMV{JS*U6@SVqNjJl#>T?YB8+pp)V4mc{Ky=1fSy>T;- zZ$|m=s((V#w8x&#x}Evtz7O?aZyZfbWChoKw_iM13ui2(CJI|Ax=RM8PW)7lI9$C) zNrk~ke(L9^7YiGdxC&O8Y&Ed0qxzdrP=x=lQahY-34Hm7CLfYR@RAaguAL@(LS# zB{hvWri2#0JDbUIwx1(0jtooP=aJjP&ykiw@;J!Z-A|q$Qa7Nxn{tpGn8;;rm+nat zC;0}|LtDaf)C5V0Bb|y6P4H|m*GvSZVi_8d5~nf&?(1b&&N=qwJ`lBiX7pqtGy0{_ zOXYsA-Cm)@f$o$~FNjvhz#A)TW%}+>>{tZJmvvWp^!7CQRup=8XFX52ve$uUXqphg zy^Hwp+p%*?`@E5wuB6@-6|WCA>N?F#$Wq0~g8&~;CJo(-W~u3q#c zL+bo9wf({f7HS|E)F&eW>Nd*c89ax!!27X)mKVm}YVQLT$(1rLLx;$Bd1xi=&E5#p z`amDgfcM7cIP~6_jA_&2x0GRuBq95k;XNu+3C8k{>d&qsuB>xzGv9;^E`H3Ll72)& z26)@98*{t}`Y~B>74UOdS8K^($!{5gqt*92M-S|&sw z31>Uc%0da*1WEzs&hv=cF~CbT92Tce9hBn zba$|buHvZttiV$ZJ2~pfi`T+z(djc3xAVUmE@k`f9w#c?vMxLy9%b46p=JA*pK1Ls zmy0Rc{0fincU_l^!g~Mn-x&vgB`8^{lQohuYBEV*W_8Ns9{y~)2rNy>=|6Z{h72zw z#bCXY^i%wDmeC6Nzmj`XR(uRmIPU6MjPv)Mtynd$f1h5R#;Ms*BA4OC3}*4k10h>M z7}N?_^O5n5+P8HeF5TL9e`A$3uN`+<#7PglNlPjy;Ti;*evuahX{YX|SRz9g{}SRD zJJ*#|_zA{=ta&Ac#QQvZEB+(z6>R?_aFlDc<5yo%1&^hJfq!(9=>s*(KM4+hh{7#g zA4Pg4__=TSXNq=5`8mhwAJ5(u3p}oCSSPv<9CEEbL)0V>$PM`{E^h&K9N$doKDn+5r$1W{9uMJ-;mjOhOJfRn)g_b z-~HF`bmo6JE!og)N zTgt0lo)28Bf0@ciA2~wJpY9nkymKv@}hhKKXCK~ZfdJmqknQH)j;;!spPPl1Pl6( zK)$^8^BjSjFNxx}Wf`~8DRi8Rx=Cwxpsg~36qpRs5A_hm4Ee)lq?vTXIY0Y7L51?E z$r$5(Tt+sj){&9(u;pFw6US$h24x-rXXp4-Cg>Pz=yTg+10M95% zM;`lvuOFjSKgNEq3;5}QlF%s)xv#|aCm7D(>NygCj7Mc1X`rG3S^fwkA=GC+zP)vm?>s{tX z!}Hiuw55D0d;RLNJ6jc{k$W{ccaO3OnN2wRv>k_{oIJSXz5Hi>?6c;zVXrt7s`PDM z!dm#wIQ?t8dXuOg(m!RUi&U)=Te+!#Qk`RSZvCmXTVqN6HON@$jmZU zO#pkxZ6sz%e*I&LQu?BHFkAHVO8d)X5vN!q2c|aJb)ecRe=J#gH*Uj&zHK-gWFtE4{z&2`dv$rN13g9L7M(G>PGF^d zQ*z?wXL)Q~(ywP-R&f#wlt;{vE=+{P7N&?*)FSzX{q!1JLmk_yskQTaAjDBr>+!3_ zx5uw<>rO+^)CgOH)9W0TnlMVG6f4fD=e0$pXARUX%1rdI1EvWtMcyAoe{tgK)3b=I zly5I2a$c{-Y<%oIU%SN)Be^?}GlA zgICzk^qMa^^;<7Z3)*L|N{q8?V@4qxihmsZv%`T+=sTQuhPiop=H5jgOE!KtDi75a9Njl)y7XZN&);KHvxzHRf=`jfS=}C%F`ohMFgB5R7Tr_&b4ItsKWhM2P)quysKx3xfp=G{5%CDE=q34Rma*2*gZ9Cx7Iwp3h6C3*0B zsssNm4oh@`B4G8%$`B=yNOPF8S~&(OfbhD0<@n8{oMJo1R-~@U&&`5|)G>L3mRC9C z;faDJ)B9P~oK&~Vj`UiMQbym-#>A19*@x2)=lR#pZrz&QnCWJqmZT88BdzOjGv$F* zQM90il?*|^j@lZU`yQ8(;qxEzpIvKC{?}Gq%)wCJE05)xy&}_dU)(ab5N4#jM1<-W zG~bn(1hFcnSLe1a`LWXQ%A{4h8nq$&7N%kh$<`1&6#?h5c=NU2ZFk ztdk5Uw+fZSH5zjgb?48oMy;OSapg#ICt9Y~8q1-Tp4vL+79`I-Jl*W=-XHPe!r=9Z z0vpsGh#>xDuoGYL@uqPXFS6^{--YS%`IQVOMCDeElCwn7i>GW$lS)hPbCw`9yq0jynPl)4?Aak8YKhZd71t(&o=(4&POu6%ll*3E8V(ZUBzOVe4)%$UU#3P zb5;c>@4VP!6QoeCJ;~Ww{$SX}UBPM2ELx&HVK$tLV%7kBylKhu;LrN9hsM+n6vL6DsTww+yzb_p3a(5a6i@-;J4P znL3|JsoMr}pIN01ia=~@9jqUd2&b4`S1Lsy6jQC0t&@cX)nA^(l={L>0_c~Y;oQ+N zkV|s27FG#4BxeN2(+9)!IeYS)LX3)qoAnKb$;DYnWHLWA0ln)iDP%{9pxbk5ke zXn&arG~3KP*gmn>G(So_P*sa!hQvF}%+==e_+l3+g$^t`3N{`n7%G;->$CeM76Y4& za?suDM(0eQs8Gp1?;0VLl!4{cR;B0lb)|Jq_@RpPV}jL`*UqjVKSTh)vp{uag(qH+ zjY(oZqs1TcJM9U4-mxta-z6+jill);uj4guKuBqSb2reAL9Hs@KDH$bv5%7HPx_`G zSXEuo=Mq>zD3HYb6@?*FhsUjwbGam2v>W+eWa}rtdY1g6YpCD1ZTtK*<9-0gX?=XJ zclPu>DXDfv&>zet=|-8sU(ALQ_%9%YgkAj*Tz&fg{`h~#KnPhk>G=7}m$Vj7u07G9 z?cRl#Ow+8)nRmSI%y-_*+{>%BRyQHST-W8*=9dxMcgra$DT(-`+Sv4t z%Vm7VSyCq9j4S~OtQc!*cDpq`>mUq}(D+*Q7}H>+WqatrcJ*etG1iOUZ)M1!#QO9 zgW6eyL%e@ka1aO5d-=rw!ag0%3_3spwlMn3*QT=q*Fv( zpdJ$p_j}+w6H%I_CnZrBkn_T$&V;GZ6$p#c6=y86g-U5r)SKBY<*}k0<0C7Z&+Vp!2oJUWGT;X29+jt3L zNXkZx0vZn7d914|5)*~1lrd3~oo*!t`o1Z7$@nHIVS={hTNP%Why5zo_V|=T48@Q- z&$9mFZPPy4s+76ju|)--LZjzapRz0mvOY9T% zU6+|`ouAETg86~#MB%`*D!YU>TS8A?zlpC-N%aj7 z$1%4coAEk(q0Z*_)tcYoWC;zllwoFzE_+hOK}cx?KxQSUshDQnXY1uzRe z3%h@N<`*$vd~=ve*Fb@ai+0UBwIX!;BwP06?VQ0-0&9t*wpny0|F$H9LZ= zQ}PJ`asU@1c~j?71ex&cN&tYFl}v8$*KQ^0KRM$0G{93QUmFC(JvYAKmHZ*s?b-Wo zk{$q}c^814+p(W*b~fKJL7k%m$rXW)oW-sb)^@U0GC$9}jYa@i=@?~##w@J9SzB9+ zwe(aKL;!b#qwsph7pw(Y`{o1y$~vMeN7}pSQaB9L4lbZV0ID61)ypn-{NHQ`=zEv} zfLqU<5`F|_SSv?(>1(M>(^2NXcVU=o;Q>>4?Vpcu05IxyBer2~0%F$wIXH|8VxDSe zXGi(DxY+-l^AA%1xRLA(H)x*Uc~2Q;@qMGm_8Jr`q)c^0^QowX!xsW9+)EEPVFJrZRuY^k0r#?^0C6tU zv%Lan&#_h{zHgXLjCP26Rc+ufvgjJd_xZSH#B70DFMb0 zF#uS054Vn(nV4J}jWWk~_CazA3arIHVTmn|36LgR=BXaOZ)8Gj>8H02LkqhF3xp~O z760`d8xhQQIB-p$DanNVT9}PsT{>50ail5Yyrru}xtM+`zUNB0xYtX##B`^-JNKJ0 z%{xP%trSCz_?Jc9$~3R6vou8IdNnLgQ&LaG07JHz)47{7Zjwytmd7e|zlZ>^g3Q5A zh#QS=*9ig<%FR=&lz*S<$F0@9sVEUarJDraFQ0DRB)*t!ZRGiKXsix++&txVO8W1T6=FYZb#?wJab|0%NiJv zTv+C+QZvh5IL#}qtIzE?P_IK{w|SO=o#DiRr>%5SMcgTLbjyFQ$wV)j1IT`tJh~_P z`tqE0>~|UN0{{N~(*R54*~g6lOnLB+`;Ienjd?1xKJvv&t5Gq4`|aX`q&$Y@LU9<= z#&LKo+f(-w@4Z8NRwt2u{m9~YRN03K)%Ik=&oaYF3-nO#Ap7P?V;WPi5J_7dbBvSU z03>&16>2)Dxc_veAM+Yr9>~#OPK%g~w8^nHH1xM&qqS~Sgr5voItklIp@_TLVncgUKC#Mo2X!|T9Jw5j(5SoBjo`kEK0w>#| z-mAr&Aoq`QIwkr|dEp45k^SQ{#e)eJeM{=ieNuk6JH&53%48Z?=lh9xP+>bF#;{^@ z{GSF^6DLz%2|ob}i;Ej#oG9OcGFMeHL(Y~31o`J*UU%9(W-t_fI2D74>+u_QC-z#) z3-fS$6vz{%#o~}p*0wY-d}oi{>=}%qVAPmBz`(@BA+JnX( zY2PZM^=Tio(P$a;9Et)HiS|i>hKD>#dT2KpXXfFx(kmn8#^tAJ5cnb#A(U}?)NRD% zwA%v_Y#3PkXfIxokogO1HBYuGLrKs|w{Zi*#QT!7bbTPvEqyDYJIiR8|$P753dY6XEei}Q~{#zZQ{nkrJ9hANnU*7xEPzD^*e~vs*ejeDd7-fExwlxfBy`Dc=vI7x3 z9PKhX*LHFlaH{qy?9VKVlIO~eyY@LRm16T43H#uK6L5+PsBB2wn(S9MNOpID3 z-?)wmELj?J5e&olf0NF;JxPrD^5qNbo@OJcuvED2c4I>?J=Cswi-!?tB4|1#7rq+kUg^p8>ae}Uj(ZA zdkd2Lc=GoW8p+9X75~Lu6#`37Wu96JAYxPw^wTfKKwZ10T9|dpc5IJ=N1nYN!CwhAR=}vo~MoIG| zp{uhLEN88T^zM}FA_2je*=`BPDIyBUT*G9YaajM&c_$=-Dct{WSp~Kq<02tw67S&LS__4 z{V{*wdo#ENPO8b;u}QpZfXY=~+F4?ha0Ztb+Q*u@qB_|tqlG&($gppe`1|x(L5OE{Nh1FDEw*9{w4*t$s zILGD4@y*>7nVNOph@Xtt=+^^gf<10NKW-|B;J+N#KlUpYq>Z8ca%1F<^ol(Knzspu ztoEin#!o)`E~v~^`Z8~r{%tNz*G}v0Xc|_6j5f&34X_%}HV|S4td1d}+lM9rwSFq} zn;x+rFl{VK@cU>`jvN%+nfRAmPfGVeXZ$RPPxaVMCz|^i^==ElFl-jb1VcgLGP!1E<#Y8v!Ughejcj zQw52Jk(uq6al%No|HD46x#@iGso<&N^KWw&K=I99F`Jvj7@|?;8$2L=m5v87vQj&p z;DFyoVzAxgw2LW@J(dmjLEcOe-qANb+1-lH@6z6nDpJoPzv%+gruZf;{J0hlWV9`T zS%v&|Xr0=y&&rbaM3PslX)q8Bu!mtBvXI1>+r0#uGil$kv(ELm>sZ-8rT}Fq57r!c zeWZrI|4iO;)ydh-LDBy|IRl$~)sRUGBstf8Mpyo+0OqwP7Aq=BHsL+KpQ4^}Wwo0* z5WHRx4#pfTKZHZrFY1h(?;?u_RJ!~9fU@z;xrsCpt78J6`s!ir<)#5m%`7(}gR8}i z66j!z=rW^!aSK8x%l^S0VB5i0B1`va;6~hJe0-lDPzYXu{E@H*3tXQ3tG#|PR<}fn zDoD!jYga-XcBi+8 zhcFZP|I_Q`#iTZCrnV#uMPIQ0gsYB*J@*vQ(bE&t^I(1ZWstF;w4q;xH*JpcXkZ2- zv-5vsN*8rz;w8w=pd7KhBS|L8(Mg7DvM|hpQGcPlwk<^FQK^98aPq>s#!be!L`675 zn;!|Ad0f+Zyvh$t!LEmUQ7HSGqqpxS!{Yz?0p|Huo><$VDxy_40i1MQeo$s8v1KsE z63hh;_+6W_^g<``XOGzG2-rga;N`O)@~DSq+XdFzQUpal#0x=H|3BYb*bZ0>R+M^4 zTZMC1-mg@nf}r+!G9L#CjZ`Vq?g`giVHjNz4d=S7k*wRc&GfYQdlyV_cdDv z@B$<0_ZH?_egDcQ*JeGX8_HpQLw$J{i`a@_$=;ntwPz z0A<>m&+%icpZ2HR;<+AQRLUbi$xq)d?Og&>$o;Qf6B9p;eU3YXMcX1>->t@JdJwBV zpn?Fy|DN;>toyl5l1iMOF>jPkLY~Dd^0`jIAxamrqh?D>bkAyEgU@7wk4hOyan4wcuS4G!4&Q*q^vg9odvK3-LCW zRs;<42i(+a!9 z6bb;w)JcM9Wv<$UHk>s@Oa@7H3ASqg%XEH9;?c#liD>AZM;8i82mtwF5r@n?ujQS! z#dA|ocB~$=vH@(!%u`jfuI=&qTT^>#AW!gGb?hHq-T;1B#U!5h%nl81e%}Txp8qLd zzi5`!)!GEDyHrR-0OTc|-L3dR$Fb?{3EUVnh!Fz%Z4jR|iMu|sN8GIl2nl&2HJ0m_ z3W}LKiA*;sIxd%@!NBpGUy4Ntt?ait;yq(kcZnU803mLdBz*NWShCOaxQ>TUFmDk; z9iTl!Pkf2CAS0jRyw;)&K&ASzf<97@@~yvEF^Eg8&ye6)|6}e*84$N5`r^ef~*MBO96{_zHWR2jRf)say;NPK{eJS@ZIc|5R$L?_aZTrbql6Es-dhF z^Q})fpT8pKkK(?!RM9^T!sPhtC(WrfX4l1q-+KiRu+k4Fjer*zwJ|`k9a-P+r)bNu z*_bWwzbJUB*@P>0nqildfB_E0^n~wjo^Shn9+gcn`CcBHD7abDNe zuk){0YlP_xI|!$(pH_`fg^e@|RbrXmYq8p_J4~;W{5|gu@M&t*<~8)I_#%sHZfR8{ zWSbFsbP=nvOKhPC^u4zkI$d2Xo_m63=eK5m33{?;<6JxY?>}x^w7mSx*^HxO)o-1T z8#Lzk{&S zlX2Jm$oCf}-+ibcbS2U+$nY1RpGvvry=z-uuSP^K-8%2?^p01rugtDGeK`uA!N)gq zHBFj}1YP%4cID|>n?w+Khb4fR7j%I%RNXJ{N#}NF#v9|bjIg~N&l9rs;1j;1Iw$YX z7FZSBe@=Q(Ng52exW4!pB9LXBzwc#_*2vl=jXU`;zp^*c;dVT$a$u!~!F?N0LQk#6 zut?`@+_gF2$6KT0vL@H_=+lf3NT?VBN5-#vqtB-5NS=yh>cwlPMDu=_f8fv=aNU*( ziqVp(ZPRv-?*DpWSphcmmbYLUGsWj!)?+Ka-(SV&ALjPX8pol%4X#a!mBwXs2b|4Q zO2{^KDQ(SQfx%v(mZ*cE0z6X`E}4P`f3YSAspesur1hnHnnpS^OWL0r(JwkqVNOmkyf(zead zv!8Od$A4YE9`$P`My_t%uu8JW9_z#+0JoW1scG!@OE%c;3L8OKcy!l8>yeevAL3Fk zXrGqPa#yFVv^@)P`}IfB$G0hE>+5!b#4**0-`93z*F?rGGFfkBo>4WUcN^dAWUD`= z3z(rXei2`%6@~)?*-%B$V+oJHzXezu{7x9|R53USMoyW#{jEX2qU`H3f<7C|Q-Mp4 z?C*Cg>odK`Pp9h5=Ls3<&L+vD%)C29_g>EFc)&lsj-8dtONm5VV^6~9iFL%QLo*rS z-ZEdatf@g2{ZgmRXKMqdHbkMou7yc@N^-!IvJ>m0&aQglY5fKi&{swob1*!t;(2M~ zP%W;Adg045d}hDA(63#yqrY}?KVXzDY3Xlv%$6i92m>>7*niQVIBK`hqjRV4W}YI-dg*VeDM`x;ef0~LCB6Ypz*bt%fJR8jb$W)V9E7F;I6jp0kzmngqZUya?`yr9p| zR((m#!W7|14kc{FJm0|q3882mZQZ#-Cd=5Ay8PJ4)c&uWb(FOdCVP9zVETcR0F!h^ zNsuxexVhiA1~{zW*WJ_xqm`}E{cCRX{nhq7hvM`6%Ejl4nG9W{cNs)3XIm7R7KpzbnG?|CH8%-Z z;}B5C(FxU0F~8Z#{(iFt=W!j(tS5ycRbYpLR5|ZZ#7Hu4GLt#Doh!~~$q-e{HacXw zbrr;9v4&Qbw3(*9M}UKhPt)701z$O7DK_PSzifbQOTiFA_PfBWJsJ+-84II_-r0Y> z@?#Auap7d!4whUyGUeOMa zK?djf)bNXeA6#2rbNh9gis-(Kh!E4a1(SDXwhMWmZit*4*7(h|@Z1&Ss zmnV1}IrV)U2Jpse#|n0S;n?V7b}bZhgi2)@8CAeLsXmLulN5rT z(eL6Z`aG+{xtmk(&odltd$nKv(}#0cFZOkPqWhcRVnB$x3@HDY=vb;fv8nxq0un3g z(I1@{_0mnCQ6=A+;qr;rtIOkea{qNp!^gJp)TgiVhLh?3PP``9$_emv*!m#7HXu7@ z<_KSbhpyr>GY=ka5bB7*fa^xz<&{fcksMvb&GxQ_;x1ZL9l|i|yc!3j^k}9HVnb&x zdVJbhv&Z=-yA(z!i~UBP5ogMb0dQRj&?)!D_f8)NL5bJNFF~Zn$RX|eX{(h3WS{?~ zZA1WTkBz3)DJcO0+uJZVqMrvVC0b{tFF&fA!t5|W&g1bhrGtx5bG&LW#@6?YEyphJ zz3IK;xX9OFw|i+P?yg#eWB*X{wSSn(waDAjhJI4?M{1y>#Gm5t@ZidNnD9`=iPFLOTKr` zwt8$ve&~ybLx_EG>NGkR3esTr*NT2Pj3xe1dbOuTk+-c4*QNR6YbDom6v3hH!N?cj z#|u_GJB<8qmJ}>QXi7s+5Ukn`h*o^bNOIGi;X#A;zl`=bWfXkMJ2j_^`0npn#*si! zRx*J?vW;W%Cp3hhrsr5hnXN`I1RVm-&7NrS{g5B;Q#f{1pV-Wv>%2)0q1sqc26Wh9 zz&r2f7IA5lB+;LreuIGLFSy7Boog~@pnD!r_xGgWn^)D2A-|M%kVJa6C^(1WPahKwJ!EQzm7a^~lfm+eb8|6N5#qWr}4 z*h*S zWnf-P%J}cSD*D=Y9_&3trVIHz;3NuZDk7uw1J88ozSH&E8Ri8>jc_gBryGzFU}@pvx1;u1RTCl$jCU48|y$8fKR*f4p#l6n5`f zo8gIqF;hpyTR`y=WT)eWeFD9rI1;#j5Uc581V(3&9KOfGL9`fFhf`pbg0k5;b4X+9 zuSN$&ozVdD8WeMTZYD>6EohH)7t{Wa8A=x;+%t<__Mvn0R8u-OgWXvHffyhXV_R5APsw6r{DMtONEUFsE11TR81~ z%=?YfiPxa<#ljp-f{W38B_H33QvxLVz<*>5xl6)fka|fiOF~je3-osI`<)F{q)eJuW z3vC7}wwIoTJgJ^eS(36SLiBBr^{;8kGwZ!cgj&%+QOONI+-rEPHi$37fZccIuP%d3 zGfZFRd$-aV4&Ok*B|Emg^IO4JA6DR#m7}4mm3PgwwLM3D0kO48*kh{RRvUZ{AJ2|}&P|pfvLSEeOvD;;1OYf4LXd@5NT-J& zqY%Kil_2KNC6u8ERcpT-G1bMPNe)a=)0~x}6Q?&V#ctH<4(5 z)CRFXZHL+ zga8hS*qloTrqjdMp_T-ylYFPIoNB?8q=*s@yA-jWnUNPFa zeKU8^O@;1;0ifggcAtisud}{gtS70;0+2|U&V}Ugb@Bl^&p0sGc|X26HOpsEDN)rz z<+(WI1n-z1L;rpP1B8+X?l{ET!-8LYy{-tf#=6=|`GJmw@suPR+K4U&=MNFFnWC*4 zAwNb2iQ67O7wPY}`f=c5Ke0RshtM)E%dH;_oX#iI6Wt|+AjyWr!bB=wuM5I5LW1zv z9IdYvM^pfa(TC^JHEwsgDZwRc#7saq0^z&y?>#yI$~-CCWH?+5%(an+A{I0O?oxI4=@& za8lWG{-F~bug-sa?PBzY&HOZA8o3_5tBAsEOL*;Q0v~55^$h$UMiL{Sg*PDpd5`He z3LvONqKyo4I0=CPA>j9sg5=yH(KfJg)5dP0g{tP&+B5fA*mBH#8_3!r+!PSh^Eq}5 zJQx$OX|eBW>vIEqPd5H&k%YcM0lwle{T9fP1Q~7GYTH9^&IJRn$WlV>U4UVoKt6$u zuq;k-)-zu*kS z^_5KN0=#If#+t`r2uA6*&L!jpUjb9vQ}Y5%lerv;V9d$Uq$EP;%2aml?Sf$AApCvj z5{(HcxZVcEU=WD!#Kh1$q@W(fSuJ$I!1Jf~jsTz~-7tT0s$u zkcd~Elp;2X8U|pzUQl6CaEL?TZGX&L1wcn^@RJ@8QPoDJ{= zlE9aAIaMdI00QbW&P96*WY3sRe+dUWixOF4Uw{_uf^s_4fL+iv{-`EpM}bH?z2)Cx z0B31aM;;px9a<>njZ{L$xtm<1JSs2X%2MH27QhO8%-4}_H!J20=k9&DCy8l( zcYB;S6oJYdA;{r|0fgSIgM8n;uz&ipvqw@J5IV_WvH%bf$;UH978C}sdH~#4$~TU9 z%U+4s^$HGteO18pZRrEI7ZD`{hoJrsfQ2aeC=NxG@|;>G(3g+RP?AGSt!Lwu06B;b z9Vnh(#HVsqIFd1067#`Fp0H!=>}$^o=G9g2g*+%K#q= zx8!k|lk3D2BuskBKG%hFL_=h+zvI3ctxEGSQmnrvxg~W4zxB5$Le(Nlm4@b_#;v29 zivp2(Z_3|@euTsSb{$Th;-@ABHa5Fisy(I83;g6~tvsjyZ1`?<*H15Z*Lw$H`m1EP zdXGzB|Mtl-#X7*N+pi2^FR;VZam+IefMSuY7-bwaRu~Xhf|OlzM@gvOl`t=GZ`{9) zX%}Hr7viJj&VC4tllkTXBln-8NPxTI=512b&!`bgVzP>SpUK)OnM~Th?%zhH2ZcI> zK+V~o?Me%B-51S&m2pQVTft0@wDWd_u(=N6$OD17B#kUFoL_$Z~*!{J@n$~ZY<1O=Y zB-~m)%x7G2LR_uI!e(I8()=vMNp=5)5_Ut)^5O$OtK@NaP;hU4H?H zopq_^Am^D|?kEklg}>$m1MmRL66r;ReF{qZf(IbYeu!O`<^h6RzE^QRM}Vr3WI@X4 z|C4pILt)9E#GrDrrKOQ>9WkdXH|`*;M8^TB)IYW5r3DyQ<5~R^b0H$WE>*Ci3CG=O z)$rJ7jUt`2@8wc3FSH2%XVEbK76FHO?^*UE06rR&2KCZ(SBurdc2Vmj{1O2?6d`5t zjV3flUJp<-55;u>ctrUNeXnf`Cmsm@&~}QAShkcp{ncjUdY9-WqCW=9(%Miur#W^A zNyKjd(a0*khv}=!cr|g#eN>YSDEt+W8?po_lJH)$I+_tvn>_B6;bZuzxsk=hC((zf zZ-QK5Or{&5AiZXx3*5GobTH+LCO&pj{_{}hou~IFgmzBx^DhGxfD&#Zn z4a5Z3|M;A1MZ{rF6-3Gncz}WNK3A6dE;mH;h8;P!#D~V@wO35+Ny=$Dqmj*HXc~iI z$Bw1MMTmp7ZEV$>k7y1glkm>cGN&~q=t=m!vrwXS!X$v4ci{$52P&=cRRj>`hMa7t zg~I`2C^h<9!&J)~{-*>${)sU()cv{FH%8n~@%%QxAh^@oPXJO{yd(+CnSBu-VrP*LA)Uye_Cr&lFlU2e8EKUXs_> z%hdkTg&Hn2m4R@h2Dor9c8`RTx5M3pFl{>}Fs6U}(&{SrrEWyc4YX!)z}5v}POZ`V4zfEIa~ z>ofo%M~)t@nHKXFYj%&|EuU~44Q!1j`&5C%c5XWASIrW9E3$>PXq36RUXX?p$OD7< zDLu@a1SwKB*bR&}F9l+@i>nRplVCGAu3ZD7wvjDCGF4v$b_;Dosr!;XI^uc%%-v|# zKtVbX#K&SgQ&PS}e>SA2N2zzV=sD#k?OyBz$gD&hd%IaRD8&xeY#vtzJeCrK5Yv|* zXY+6aH(vY&2+$^QBceYQ+rAn65GpiNn^>5u=j|!gXq*`$9X}o4ZZ~nX`Hb~xmlPKy zj8+%8OK3DfE_{d?sXL&$N==gbP~4FMtSOa94xjFocAhd^^~ZUk{ATrK8F^Q1(!z=}gD}LY(w( z{GJ%kO78h1a8E*{5{zK&F0hlmSkCkSHFt*gme+tFvldsyC`Jqt*MzsJN$%ev{#@xA zMr*|;-!XuB{{|YTP(UfF49rnkU&W#ltBUjdBhK2TZxH0v{E>FvC%lP!QJX{d>mM$Q z)!#GI$JpD~$ig*sfV&+@flq%VVHno-!V@r2oX#xHbNo}&f5U%MxE$l^*1+hLTYNqL z+euAYqL^44Z$k|_;(Hk^N{4dJ`RjZtHDZ1%= zTxMV0nDalnaW%QfnWcP9+T~|eOu>>_nw8fk&dOU`VN5W{Q3b+w68s6UU6LDSjAAh$3jG z(Ef%2)pE*t{f-A}UY>yn`|jfj^W=|6KW;K<$Dlc8QcZu*@zQ|V$=2K}BCx+fgKx~X zJdV1I3*FBd^_s*5(&Sa`v)zGbNCLVp5+(r7V{`7m5VX5yhD;kakK!}T9j}gUo1|N( zo^xJudex%TwJz;d1Pw^U(3~$JJK8aJfzX_Y(rChfd`Du9Rn&e8ESQ~c{o0)Qt#7RO zm14WmOleneeF7%&eE*8h_Su9#$#wgFM``F2>k~E+LgGs_$8J-TlLei#(DY8$ter!IIghcU4QR0GcJ&~WhZ2(c$X*u zZ}_uWodnyFMMSyUr5n_8T6l`<%}#OdBIQ7`HMtv!)R_s?x*W-aEt#n65G)+H&BTP+ z=9o$N1x-$;nG+4zQKcBtt3GDPfwf5bnrF5CzMXir^u9cS#d+$<3K`f{;W4*nCX_}* zkUh(@*AGwBHvXYiV+IWrwvsVf_lROPe+}Ska{)=r_a>x*^tBag| zPib?V-A`7fOzw}~LD=jM zwW%7P__%jzJOIXcA@?9NbwOTd(>yu=lVZsQN24s{WD=c!LoOZ@(Ld9~8YJ%`x$dFn z{2T329q_004K?QIKKAqZa-Kd*`-vnWDJH1KHZsu2R3tr%dnmY_!~6xBKoBh*J|{y2 zg5e>ND!KY`@)o4u|K6%Fip19bGAeY*h!0tt14xGF$_CD>0=@p!DXWi2B%i#&M=cAT z&@2yds|K9$)|H#<8V@&hTVSZoD{&)vNXqTbX0+BD=!}rS`(z|Q8mo>W#jRDKg|*LV z!tyCowwp|YY=%V*`CuH_yVHhH0j7lBW;A6GND#yP+*~;#g2RlHZzmk4aS!mqZ6G5@ z_PGRR);#T8rnx@%91uT1y@9!Pi97co9{%fI{R znC9%7;PmOzUzKEQbFaA}pHs$-t0K=+D3@pCxfCJJNWxKUt8QRqO1vS`v7B#IeAiMH zT0jBIN|~8{2hsx9FVJ_Js!o-pOB}WXsmWvB-+>aX)wY5?I537YWI2#tztsfCkiM6?ZY!qrg>F7AUmP}2#h?$%XmQpJrHtMfADT}MPoh)gV(WV4L;iHm~_D;|wJ%egDYVZy;?&azoOaYuZZe^r62rZ~>*#2n=T+6HR=C)H+>O?kmS?q=X zLsYB6#-%hfDdyLWoF1qKbjy!jK!hS_f6R2KJan-z7n^89$Z^8O6NL!d89P(WIC?<= zCPdE_a(QY`r$z~mnL@cyc=_?R7dId4Ww}`IIp?j@ z8Wwq<2D-!enm^=n)Ya~d$G2xv=10yw31|WOz9#s|tmsVJ?_fg8cmRwEqA!dZ#(ReR zEGHk>_z5PsA>|jzP2(>9--RoKqXQXpfSTZJmX|YGr<_&8oP2$JkakMqrHs}Y?Tqer zqo0zOA8&d`dz?$(!}5E}apcU+C`Mfmr^yAWSO~FtAF;jJ)dq#?&=_VS88%tG%$7<^ zZsFw7l*(a^pDF(r8rBsxlCqBN3|NM^5OLU{dC&~qNy_VMDVh?_$Bdj$ovIB|pOfgw z;<~QrgMy3gjsLjA8SfC-`~9T@8ywMFSN%S#w=4a zzz%7Th2s3J5kDq;!wDYMZRK~}bXM16uKK59D3Aji;e^ODy$@?Pxwi}w(`?=C$uOU( z8rI}9AmFEV{&}AOFC~JV(PrxWl6lQovYJk?gz{5WlP;8RZ(I<<@q?)3M%Pr+xVgyI zUFhqw$$)QxpR3<%wtH}L%qXv(%C~`(;b}d`RNE3c7n`*W!)Lma(>xHm)6GQ8o(m!O z+=5w2kme7v_c91c-@HI#dbf&4kjL*ZqxoPwi+DfRV;Ar(rhHhXVkr0lhIJ%iFrjTB z{Pmi@F<6pWd|6rPymUfIwAf$v}1Nrb`B{w#ni05G&pyv=c0RBpyYG4 zV<*QSBo6c__!2X)q-bQ*)gZ z6Lk+wy)%6stF%4F4*A3aley5u6QSKrPCs*S;wmNI{NQCDsgZt8cp6>12&hqjnXLxU zVxUXPic9`-2EBgbWITd7rx(j%OSm1!!wqp*_@oUz0u$iAGUbJ<2az z2XhGr53mw#5VZWS?B*sdDBfgS*?EBR`RbvXNaBTu^jhG(Y~{4SIdiJUay(?y%&aJS z%J2t_QB}&$lW%-vYCl|y_Vwr&hrWM9-KQGhb9?5jls3ncC3?6U1Slqrai(({vW#Q* zw}A@C|HNvxGs4eyMzX4<-neGlwIpD{ZY}}rBHzk#H$AcQQS~w1@|2@ z!@=@+Vf6Nx5H6$p}FIcQ_@VQxhewxi5(fcyLPlFkq~1J0hSo`?|3UTCpf-wCqk zh|{}Sv#d!o{x2wK%*FY5mDE_~dX0t;&cu)V@JqG#>i+YK-A#i9ZS5eY_)=ewXRuy@ z*c|k*YQ{bB@bB$}LRG<>2yKIaZ~FRSF8Fqe)bDxM+{+a%SK*fG)esKkQ;$e$=fe@d zx8uEG!$h)hLaarS8>B=Y!Yw_7jml2-a^Wf4ah>_j|F`aos;*!KHx+{^lt~JZw3ZZd zt(onzza+hWAs8V3wvN7KS}6-hxoc`@iyKsRDeuOSm>ZxPJhWPCqzk2ZXWZW#r$?)yQe4Z0| z{$})ZBi;744&$Ku&qQ7)^Y?Whldljl2_rF|3CJ}zXt2!<6bn}O^hv?g%{PH)zBs&A z&#`(~`TSL(mVew04y-q|(2v{U*>7h*qe~Sa$VNp%{Px2geB$CuJU8XbxZ#a(5(U?w zB2}|keqm?lQb6@kRLC&FN4AVRuAe0Q-{*_os&s?K!IrS}TPHJJOBGM#Bn(>`VlEv4 z4(tu;I93?$wHLkC@wx)2y~LIF^kN;+k2jCSQxksjCtIt0;*cwYCy*cTi1zWOJs|Bn zNUHkzD#zY&-(G`m?_1iqx7b=^Rr#sj;av3~#K?Z|Yee)Q_RNHHXe-h@v%Vw=TUs)y zmN3W~ahiELIalR$d`4mg;-v%Pwc_O2^_-C5(Zax^pDbZelwY9LPVE2F&Xop2xxW2p z#*8ttFWIuzWT(x(Z!s!+p$sZ5R3iIWW-x`QkS&oRibE=#EJH>qqElHy7&A#B>sW^{ z@2zwG|Mz@;Kfm*J?)!S~>v!$feLc_DDo(w$!|37iJ^P*x^0{ELy3Rx&#Ku&CEF%P2 zRX5|Tq0^v_A#;4!&W>Jyxz~9k>^CDtL2YTM!tz>j5nt6vd~r>~KlLEM@UcsUrRp&(r}*(PqS$l$D8%pU4{yT5z47v_JXwM% zXbikMz-kLP9p5+?Wo!EL`{CLWL{0l#)>-clUARx`_q*tt+b3~oVZTy*RrmC-o~?Ha z`TcUv@WDl+3}WzfkY6dCettfN&?5+wlh|v=YXh}Yk0OsE~Ywb>YV(C{dG6^2l2dHvrEJAqhnsp}%O4O7HLj$d`r!=Ur3m{ma(r4@O5)cHeoQj_M~->MSl}ZQ6APGJ z<-@XZcKE0-8v!ss1<(YGuY669u9V|4#Iz7B_gioLk_J_v=rJR9+ui&Xdn$(a07lOJ zr3FUr%WCnH%L6MzW!0N^WZVN|*4DvVC3!L9AQC3hbpr3n0Cn^zGlgK!>dHD^U+^(=zMEo*jzsZ8|{eOx4Dsw$IP6 zEX+TQ4=)G9ImLimpgy}|bHC4}>zSUljIJs(^Y0IHgo8RK zLyC6dA;RY!eeUo3>A#a+95o)G$<trm35(=O(>F4-kO?(U0pf8HD6Bd{Xa46+5-4}|#ojByJhOo;cVIl1WgKd z{%fZbTB!pjlY8&ZH~!%0pczaScKT}%B>f5ns)ww+)*zk#>ay#L&OP42tI(E_s_gF7 zZ{8*e%)(jjVh(#Q({ty(CloqNxX_m6q#s%rTJ8PU1BB4J(QZ3u4G!?*sv^4SzNI-2 zDSVSuRroyYDl%B}VI;;KSm56Z9eB>b4Sub-n$T0@u@T_GSMX!pK?(XWY6S3<9^bdD zr1)5?{Y5sfErcO|UPpx(yZT^ z*LJ$)W-pp9I{+n$ba=cm2T%>vpltG8xMF4XMunDXgL)tUUV2tpF4}t+bkoI(`~%lN z6mt59e|G1+cD{oXLmd|)|5$7e!E1bweVwpYM~e96`9?b291_XQ%)A;PM~K_O3 z=l-+D$=_3;G$$`!mH*x0eN^WyfAjzDxxP^JBV)*ZTZDBh;*2kEsHmymE+&5469k^S z;9YQEU}m{IawBPKK<)VtLW63WhT(1qd_TiapTV}#KeoNK{UfK00ZMoD4mF$d@7+T+ zAIpY*ZC!QZtIW;0rM9w8>*(xq`-TPtxc66&TNDZ)sc%vykcN@Q`=;!R20v#IdpmEL zp>3Lm>#BMhKT3CL5-GRezs`}FUZzMS?bn>Bw9sefLXArB)M){X#@A0Us+0-bE(UU| z0UbtsJ-Ctq?$I?=a;5&k8fT1wI%@p(J5Gbm$~Kq%hr=-4NskfaqM|dnh=_X09s3-? z*~dY)sn)ow?nKnDKDOk=6kQj1x*3{SfT5HRqXxq9CQO7*4JGePt}4zY9I14NeD8rrM4Jx)O~B;b#$uj%SI!q zHLESSU!E532yH3D19pc?P!Fd3t(TRviPLdIN-FfPgARdMew-o~_eU z#cbODv(Gp=BY{tA~g9n2u^-#@HEy+J}9b;F>=e`1|f@3oq%9=mg$Mp zlVjRq^lqvP}X#>S}Ijhm0Q3F$pEm}ltlZfqzCH{&J3mzs11w<{-m zpdKEK0giaq=bFpA1`{|oqFTQRIlq{wBuR%mL0~Rn6=q<$R;PJ6w;xe`RC*tgXLPyy#5Q zEPmkn1*EB?r{|W~8poE?$Vx9xtSG^Z7JJI>fI|#VK>z){Te|#il-;K>wvZCcZ_txb z#Qef)VW}<6q)+o7j%wYTBv1H^LmMz_pv3ymiO{ep_w~xpfJ(N4?=T^>I9Bxn#NPVU zdgUp{)ykWvVuhe)TdFG_8BmDWtUio#O#O>$pPM!i__#IIkGA|w*nS7md@C_55!!GY z`7u?wqsu;2K=*8;JQ}pFg;(FBWIVX09qw{F!Wm-d_Ux>=8bPx>LcmTwdcos0tm+@f zr$6D)6lNpj;^J~yNmH}=$cSelMWfZd6c%4g;APFIG1A!g>e$i@b*+9e4CI+fd~U?+ zA=k87-G*??Ya21fcH|2gXajs{;hx)))v;+X9SQBiiH#aI{D6xc&7$s;a9hCy(1-TQ z-~lxGLJTB&Vn|ds+GpoWJ^e(BU!3V%OC-xL80P3RG*Mn%Z2UKHtNqSXjNyp(85e6ag!pU>+1$pV>0=82u=XrE`&kTZ zm!i?ex8hopLVNPs6nfGy8FQHBH>&G}Cg$Mcxu}^SB1_Oh8{7JT}^o?;5YrMz;Tf=K)V_sclln z)P@ihU%H}-$ViPWqA`x>Xmmfm32$Tx-^>x>xNewlf5_S0eQie)8l+MQd{JPAL~Foe zYo4B7>l_RSi-jbZE$YHK#-q+%jlu1H7FC^OUkgvR;?9afR}7!mrp48TDn6^Wd-8o3 zTB#kjZksCW;NyKUj03e(N?fDqSIZl!z}kEF)kKSk*H<~%qs%AUxti4&KU*5RqeTLq z^t%18K?7>QN_$^Rkad3U9qMI#RUw|{a_iB4)X>jdMfJfGQkx!^w0Z3=@4K(5rS$w_ z%Aw|`nXC0?95-xCJx?rtiCC6Qd>Q)gcr0aYYh3}&pp6}m9?|q#p5R@Yy7}nwkFt9! zH^muAzXsH2*kQ-jKH&s$tP_xksHo@>i^5N$>08AV4N`F&_gk7RPl}pjfp`=eo7vIP z(dokT3CEw$IthuKA+C1Z4#;aXvI@g-$&-9YdfhAZ#}uIk=I_+A%r{O`1g(42c=jp9 zm?U6+*@_o#;`QfdOxr4)oGN|#E*G_|%DL1?!8I%zY#%VsuUURvR`xZ-wH56E6=h^) zz0?Yu9X?_OpY6keNj&*U>o|5vcVF`pii}+>Tp(dud^P8h9(E+g_4o6OGf^&5Pbos+ zb|=*o*u0HeU}9rF2o>Pj-LEcnzaV&pVe&YUaBsvbQ_}S3DDQ9Q@CRBuls7UW&7!N+`hY#0i;l$=g{W*$0dNim?l` z8itVM{C2)&k`48p0*sw?XRTq%f=j*_snzwsB%WYU2o)2j-99EG-V33_ZH+^FaIyfq zW78>~iv{Hr1%H5!LRI|nB*ZS^Orq!Sy~g85z~KUJ(t8V`U`VT~rlR7qQ$)ntBE!dw zBv6!G(#WJeErCV!Z3!^Y@yA<4q+w`-4w zQqKaZW!$0vemE1&Us}JZ*K~E93n(cX4dQ~%B>sVMmkjJ_AuF_>UZ{D;Ic5N*O(|ne+`zkB?xX{rZ zz&e&0)3GFhD$U-rQBTuC7egnHS_Qkl1zn`ehi9lIB_&~?Ixtc;SfZtCo%&6!&`^D5 z4AUKP@(D#Dh6qoU_etx&1icOkkV-%6d~|! ziUca@dyJPQ*`umqs!~{2DSd9WE_eY59jT#c1VLE17 zrmn8uWNpciNhA9D$-v$jA9s2=+aPG(ZtP$`;aqD(S8ZX7`hGl06IJK~U5AuhM~Ci4 zAdQG~=gwu{yF_qnJ3mE@*BeB05ITB(lS>{ELU04M6dV&?CRFb-E+<66fPh z5RC2FlP_PupXqb4QbxX&P)cUsMr;tu_RRELXi&9u0yn$nW2V16y44p}cBa9!+~(^* zWV*MYf5eHu>CfGy@SH2@pxS4r7mt@npDd@C7k>k{4R%W|L-7mi44{E=AlRy=KeEkL zSi6hi?e(xSPmFZeoSf-6@0q>S4A_1)Yn!%Y3Y!=BNzGWg-A*-FVDhUxt*DqbjgZhZ z4@k9L@GL20&>sj?Wgo_NVxpDIwm7h|9lusf)ZNQ!Pqx~LBMj}9AW!9|_N@y5V?hCl z3H7U}@;!42mL{u-j~=Ao#C*5u$jlInmVF?R=|NlS#Zy;>R#%DA%chp^jwtrYFJIhLQ@3{W~cG=#Y diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index f98ccf1f3eca7f764f5a066ae22f6b88fcf6653f..d2bd35cd1da49a6401a7cac00f58241c4cccfe1b 100644 GIT binary patch delta 1577 zcmV+^2G;qb7OxDD8Gi!+005o0f$RVP0rgN!R7C&)00II65)u*u0s;X60RjR71_lNR z2?_T0_Wu6<3JMAe3JUS@@c{t=`1tq%0RaL60@T#h0|NsJ3JU)I{{R2~{QUg?|NoAT zjsO4v0RaI3000LE2Lb{D`uh6({QUX(`QYH-?(XjT`ueuEwtxEi`u+X=udlBJ1O&#$ z#^mJW-rnB-|Nj7k`2c|W0IcZ%ujl}_Hx9l0D1iYl=A?7`v92n z0G#gty5s1H0Jr3Ty#7F*{lDz_0J-Jw_y1t9{1J}$VAJma zp6@`%>|m?@0C@f&wd|kE|B%H0zu^AQ?ET>P{GZhPpxF9Aruv}W_;APfK(FOn@nFvIaNF-7uJ2IH?jW-6 zK+Wm^wdSwX|8TJXA%^~Nx%_~|`mf~qFrE1zm-vv__pj>qfYS95p7If)@nFsHF}m*& zr|&_Jh)@5W(c_%RYJl001m>QchC<7X=hZG!hBu;SLbnMM6I>t|kWX(6W$o zIx8O>DD?8w$Gww>gJE4drJ<8s6XK8n00X^AL_t(o!|jxJQxicL#$!(JQcXytgT1>z zYzX3kQNW-m3PK>kh|-Hx5u`~Iq=UV8{hVIeO@H3I+hxdqj^i`T5Xd~gdG>9l|N85+ zGdU^Ix(hM2%bJ*UXeZllMO%{fFx{Vwwyb2DKM}>DM5F%zf{H)GVPsGsIK8|UiA3s` zf&pZZzLZwI`)yBej?b5S>eR`Tw=Z1ScxtGmt^qpxr`|JX&*tR#DhZ*yZ*kKIhY-~0 z>VN6eXJi7&P%9D2`&tmx0qFN8A)wBpo3{WV7iTD6(EyUX03hs*eNom`Py`8;(G1!; z>=gjOW{vr~uM`yAfdo>?C0!M50AP>J|NQy$5~0XQc%a`49sqbmG?7;U3G3f_?x}Ua zejqBK&7X$}cZ$9*j0V(!`s&Li{<)PF8h?O8SW|_VaHU|T3d{v@)l1zZW61%)HvGTV ztFwNU@TCn=R4;dPkpRp_z~r^7g-V9c%~ZeA&ENR1Z83lu16oT;Fky`5e}*UOBLW zfZ;4mcuWTb$#%~%0HKSRP(}v?q|K_7ce9=WsL56dlZc^v5<%YXHQWUwKq8cu7Pqqj zDj~v3ke{z)c*g|b-Hb32j9gL)<19e0y4{sO7=XK%FXd;;1V$07$bDF5JfZO%CN$9j z9pu7HGXMkUR6^EkI$(j^s37ABb$^c)44|B}YEcv$sQxj0X(hr3_C-Q<4@Hsqo2x+my7DRr zVHF`-4Y2yF@8eIQ?2DnHGR586kV{)3U;9oJ1Al-T?15%D z)y3q>;SAk~Xb&tRXVLlPZsOh45V~U%V&Ndbq4783c`#&{2NULi0Div)@KPiMn8EMw zL_qs_0I(m|0Q#kFHW&y~+FN_V4gj_zNUQs=q8YMOLimktL5|n}K=4?Q9x*6JD_I(D zAJXra#UlWKIU@xbR5#Fgw|}OlW+XJ+MA^=R8D0|*?@mB*DZ$N|AwWn!>WIhhIGQe) zG_q&9;>eKT^4LsTnYlFALCeKPUrSb<#f7I-RU;$*Gb?qJZbJ7o64Q$$>0PNOJ2cd=Xp+%{30v*8UOIS-6nWV b|Cs&&CV3;(y9-hE00000NkvXXu0mjfVM!{+ literal 2851 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 a2it#tsAIaS9p3lv(b>V(zQoQq_WuBm=|rLc literal 51695 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%0D}t}!A64# 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 Te#RJt2VrvD;u!IW+pYft1QtW! From c8bee2792926b461cea0cc6c435e5ef230bcd053 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 13 Feb 2023 18:39:14 +0800 Subject: [PATCH 050/202] remove RustDesk renamed to rustdesk which is for ps convience but cause code sign failure --- build.py | 1 - 1 file changed, 1 deletion(-) diff --git a/build.py b/build.py index dce434720..9e490166f 100755 --- a/build.py +++ b/build.py @@ -322,7 +322,6 @@ def build_flutter_dmg(version, features): os.system('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h') os.chdir('flutter') os.system('flutter build macos --release') - os.system('mv ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/RustDesk ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/rustdesk') os.system( "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") From 8a68974f4f1fa17073a82c331075ed5dd2ca4a0a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 14 Feb 2023 14:30:42 +0800 Subject: [PATCH 051/202] Try out change CFBundleExecutable to rustdesk from EXECUTABLE_NAME, so that it is not "RustDesk" --- flutter/macos/Runner/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index 96616e8c4..0438f9d85 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable - $(EXECUTABLE_NAME) + rustdesk CFBundleIconFile CFBundleIdentifier From b65f940a25ebb3414e493908ee456742ecf230eb Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 14 Feb 2023 14:45:31 +0800 Subject: [PATCH 052/202] fix: issue #3204 --- src/lang.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang.rs b/src/lang.rs index f24d015e2..3dc81c8aa 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -81,7 +81,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { if lang.is_empty() { // zh_CN on Linux, zh-Hans-CN on mac, zh_CN_#Hans on Android if locale.starts_with("zh") { - lang = (if locale.contains("TW") { "tw" } else { "cn" }).to_owned(); + lang = (if locale.contains("tw") { "tw" } else { "cn" }).to_owned(); } } if lang.is_empty() { From 60fa453495152f21be622de154efdd28d79efd39 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 14 Feb 2023 14:50:01 +0800 Subject: [PATCH 053/202] revert back, https://stackoverflow.com/questions/3654931/application-failed-codesign-verification, codesign fail after change executable_name --- flutter/macos/Runner/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index 0438f9d85..96616e8c4 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable - rustdesk + $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier From 1adfc2c7b00cea74ce299676c9b1c5828291fb7d Mon Sep 17 00:00:00 2001 From: FastAct <93490087+FastAct@users.noreply.github.com> Date: Tue, 14 Feb 2023 10:05:47 +0100 Subject: [PATCH 054/202] changed language files added dutch translatoinn --- src/lang.rs | 3 + src/lang/nl.rs | 453 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 src/lang/nl.rs diff --git a/src/lang.rs b/src/lang.rs index f24d015e2..a50d2b5b9 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -14,6 +14,7 @@ mod id; mod it; mod ja; mod ko; +mod nl; mod pl; mod ptbr; mod ro; @@ -40,6 +41,7 @@ lazy_static::lazy_static! { ("it", "Italiano"), ("fr", "Français"), ("de", "Deutsch"), + ("nl", "Nederlands"), ("cn", "简体中文"), ("tw", "繁體中文"), ("pt", "Português"), @@ -99,6 +101,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "it" => it::T.deref(), "tw" => tw::T.deref(), "de" => de::T.deref(), + "nl" => nl::T.deref(), "es" => es::T.deref(), "hu" => hu::T.deref(), "ru" => ru::T.deref(), diff --git a/src/lang/nl.rs b/src/lang/nl.rs new file mode 100644 index 000000000..3b01492d3 --- /dev/null +++ b/src/lang/nl.rs @@ -0,0 +1,453 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Uw Bureaublad"), + ("desk_tip", "Uw bureaublad is toegankelijk via de ID en het wachtwoord hieronder."), + ("Password", "Wachtwoord"), + ("Ready", "Klaar"), + ("Established", "Opgezet"), + ("connecting_status", "Verbinding maken met het RustDesk netwerk..."), + ("Enable Service", "Service Inschakelen"), + ("Start Service", "Start Service"), + ("Service is running", "De service loopt."), + ("Service is not running", "De service loopt niet"), + ("not_ready_status", "Niet klaar, controleer de netwerkverbinding"), + ("Control Remote Desktop", "Beheer Extern Bureaublad"), + ("Transfer File", "Bestand Overzetten"), + ("Connect", "Verbinden"), + ("Recent Sessions", "Recente Behandelingen"), + ("Address Book", "Adresboek"), + ("Confirmation", "Bevestiging"), + ("TCP Tunneling", "TCP Tunneling"), + ("Remove", "Verwijder"), + ("Refresh random password", "Vernieuw willekeurig wachtwoord"), + ("Set your own password", "Stel je eigen wachtwoord in"), + ("Enable Keyboard/Mouse", "Toetsenbord/Muis Inschakelen"), + ("Enable Clipboard", "Klembord Inschakelen"), + ("Enable File Transfer", "Bestandsoverdracht Inschakelen"), + ("Enable TCP Tunneling", "TCP Tunneling Inschakelen"), + ("IP Whitelisting", "IP Witte Lijst"), + ("ID/Relay Server", "ID/Relay Server"), + ("Import Server Config", "Importeer Serverconfiguratie"), + ("Export Server Config", "Exporteer Serverconfiguratie"), + ("Import server configuration successfully", "Importeren serverconfiguratie succesvol"), + ("Export server configuration successfully", "Exporteren serverconfiguratie succesvol"), + ("Invalid server configuration", "Ongeldige Serverconfiguratie"), + ("Clipboard is empty", "Klembord is leeg"), + ("Stop service", "Stop service"), + ("Change ID", "Wijzig ID"), + ("Website", "Website"), + ("About", "Over"), + ("Slogan_tip", "Gedaan met het hart in deze chaotische wereld!"), + ("Privacy Statement", "Privacyverklaring"), + ("Mute", "Geluid uit"), + ("Build Date", "Versie datum"), + ("Version", "Versie"), + ("Home", "Startpagina"), + ("Audio Input", "Audio Ingang"), + ("Enhancements", "Verbeteringen"), + ("Hardware Codec", "Hardware Codec"), + ("Adaptive Bitrate", "Aangepaste Bitsnelheid"), + ("ID Server", "Server ID"), + ("Relay Server", "Relay Server"), + ("API Server", "API Server"), + ("invalid_http", "Moet beginnen met http:// of https://"), + ("Invalid IP", "Ongeldig IP"), + ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), + ("Invalid format", "Ongeldig formaat"), + ("server_not_support", "Nog niet ondersteund door de server"), + ("Not available", "Niet beschikbaar"), + ("Too frequent", "Te vaak"), + ("Cancel", "Annuleer"), + ("Skip", "Overslaan"), + ("Close", "Sluit"), + ("Retry", "Probeer opnieuw"), + ("OK", "OK"), + ("Password Required", "Wachtwoord vereist"), + ("Please enter your password", "Geef uw wachtwoord in"), + ("Remember password", "Wachtwoord onthouden"), + ("Wrong Password", "Verkeerd wachtwoord"), + ("Do you want to enter again?", "Wil je opnieuw ingeven?"), + ("Connection Error", "Fout bij verbinding"), + ("Error", "Fout"), + ("Reset by the peer", "Reset door de peer"), + ("Connecting...", "Verbinding maken..."), + ("Connection in progress. Please wait.", "Verbinding in uitvoering. Even geduld a.u.b."), + ("Please try 1 minute later", "Probeer 1 minuut later"), + ("Login Error", "Login Fout"), + ("Successful", "Succesvol"), + ("Connected, waiting for image...", "Verbonden, wacht op beeld..."), + ("Name", "Naam"), + ("Type", "Type"), + ("Modified", "Gewijzigd"), + ("Size", "Grootte"), + ("Show Hidden Files", "Toon verborgen bestanden"), + ("Receive", "Ontvangen"), + ("Send", "Verzenden"), + ("Refresh File", "Bestand Verversen"), + ("Local", "Lokaal"), + ("Remote", "Op afstand"), + ("Remote Computer", "Externe Computer"), + ("Local Computer", "Lokale Computer"), + ("Confirm Delete", "Bevestig Verwijderen"), + ("Delete", "Verwijder"), + ("Properties", "Eigenschappen"), + ("Multi Select", "Meervoudig selecteren"), + ("Select All", "Selecteer Alle"), + ("Unselect All", "Deselecteer alles"), + ("Empty Directory", "Lege Map"), + ("Not an empty directory", "Geen Lege Map"), + ("Are you sure you want to delete this file?", "Weet je zeker dat je dit bestand wilt verwijderen?"), + ("Are you sure you want to delete this empty directory?", "Weet je zeker dat je deze lege map wilt verwijderen?"), + ("Are you sure you want to delete the file of this directory?", "Weet je zeker dat je het bestand uit deze map wilt verwijderen?"), + ("Do this for all conflicts", "Doe dit voor alle conflicten"), + ("This is irreversible!", "Dit is onomkeerbaar!"), + ("Deleting", "Verwijderen"), + ("files", "bestanden"), + ("Waiting", "Wachten"), + ("Finished", "Voltooid"), + ("Speed", "Snelheid"), + ("Custom Image Quality", "Aangepaste beeldkwaliteit"), + ("Privacy mode", "Privacymodus"), + ("Block user input", "Gebruikersinvoer blokkeren"), + ("Unblock user input", "Gebruikersinvoer opheffen"), + ("Adjust Window", "Venster Aanpassen"), + ("Original", "Origineel"), + ("Shrink", "Verkleinen"), + ("Stretch", "Uitrekken"), + ("Scrollbar", "Schuifbalk"), + ("ScrollAuto", "Auto Schuiven"), + ("Good image quality", "Goede beeldkwaliteit"), + ("Balanced", "Gebalanceerd"), + ("Optimize reaction time", "Optimaliseer reactietijd"), + ("Custom", "Aangepast"), + ("Show remote cursor", "Toon cursor van extern bureaublad"), + ("Show quality monitor", "Kwaliteitsmonitor tonen"), + ("Disable clipboard", "Klembord uitschakelen"), + ("Lock after session end", "Vergrendelen na einde sessie"), + ("Insert", "Invoegen"), + ("Insert Lock", "Vergrendeling Invoegen"), + ("Refresh", "Vernieuwen"), + ("ID does not exist", "ID bestaat niet"), + ("Failed to connect to rendezvous server", "Verbinding met rendez-vous-server mislukt"), + ("Please try later", "Probeer later opnieuw"), + ("Remote desktop is offline", "Extern bureaublad is offline"), + ("Key mismatch", "Code onjuist"), + ("Timeout", "Time-out"), + ("Failed to connect to relay server", "Verbinding met relayserver mislukt"), + ("Failed to connect via rendezvous server", "Verbinding via rendez-vous-server mislukt"), + ("Failed to connect via relay server", "Verbinding via relaisserver mislukt"), + ("Failed to make direct connection to remote desktop", "Onmogelijk direct verbinding te maken met extern bureaublad"), + ("Set Password", "Wachtwoord Instellen"), + ("OS Password", "OS Wachtwoord"), + ("install_tip", "Je gebruikt een niet geinstalleerde versie. Als gevolg van UAC-beperkingen is het in sommige gevallen niet mogelijk om als controleterminal de muis en het toetsenbord te bedienen of het scherm over te nemen. Klik op de knop hieronder om RustDesk op het systeem te installeren om het bovenstaande probleem te voorkomen."), + ("Click to upgrade", "Klik voor upgrade"), + ("Click to download", "Klik om te downloaden"), + ("Click to update", "Klik om bij te werken"), + ("Configure", "Configureren"), + ("config_acc", "Om je bureaublad op afstand te kunnen bedienen, moet je RustDesk \"toegankelijkheid\" toestemming geven."), + ("config_screen", "Om toegang te krijgen tot het externe bureaublad, moet je RustDesk de toestemming \"schermregistratie\" geven."), + ("Installing ...", "Installeren ..."), + ("Install", "Installeer"), + ("Installation", "Installatie"), + ("Installation Path", "Installatie Pad"), + ("Create start menu shortcuts", "Startmenu snelkoppelingen maken"), + ("Create desktop icon", "Bureaubladpictogram maken"), + ("agreement_tip", "Het starten van de installatie betekent het accepteren van de licentieovereenkomst."), + ("Accept and Install", "Accepteren en installeren"), + ("End-user license agreement", "Licentieovereenkomst eindgebruiker"), + ("Generating ...", "Genereert ..."), + ("Your installation is lower version.", "Uw installatie is een lagere versie."), + ("not_close_tcp_tip", "Gelieve dit venster niet te sluiten wanneer u de tunnel gebruikt"), + ("Listening ...", "Luisteren ..."), + ("Remote Host", "Externe Host"), + ("Remote Port", "Externe Poort"), + ("Action", "Actie"), + ("Add", "Toevoegen"), + ("Local Port", "Lokale Poort"), + ("Local Address", "Lokaal Adres"), + ("Change Local Port", "Wijzig Lokale Poort"), + ("setup_server_tip", "Als u een snellere verbindingssnelheid nodig heeft, kunt u ervoor kiezen om uw eigen server aan te maken"), + ("Too short, at least 6 characters.", "e kort, minstens 6 tekens."), + ("The confirmation is not identical.", "De bevestiging is niet identiek."), + ("Permissions", "Machtigingen"), + ("Accept", "Accepteren"), + ("Dismiss", "Afwijzen"), + ("Disconnect", "Verbinding verbreken"), + ("Allow using keyboard and mouse", "Gebruik toetsenbord en muis toestaan"), + ("Allow using clipboard", "Gebruik klembord toestaan"), + ("Allow hearing sound", "Geluidsweergave toestaan"), + ("Allow file copy and paste", "Kopieren en plakken van bestanden toestaan"), + ("Connected", "Verbonden"), + ("Direct and encrypted connection", "Directe en versleutelde verbinding"), + ("Relayed and encrypted connection", "Doorgeschakelde en versleutelde verbinding"), + ("Direct and unencrypted connection", "Directe en niet-versleutelde verbinding"), + ("Relayed and unencrypted connection", "Doorgeschakelde en niet-versleutelde verbinding"), + ("Enter Remote ID", "Voer Extern ID in"), + ("Enter your password", "Voer uw wachtwoord in"), + ("Logging in...", "Aanmelden..."), + ("Enable RDP session sharing", "Delen van RDP-sessie inschakelen"), + ("Auto Login", "Automatisch Aanmelden"), + ("Enable Direct IP Access", "Directe IP-toegang inschakelen"), + ("Rename", "Naam wijzigen"), + ("Space", "Spatie"), + ("Create Desktop Shortcut", "Snelkoppeling op bureaublad maken"), + ("Change Path", "Pad wijzigen"), + ("Create Folder", "Map Maken"), + ("Please enter the folder name", "Geef de mapnaam op"), + ("Fix it", "Repareer het"), + ("Warning", "Waarschuwing"), + ("Login screen using Wayland is not supported", "Aanmeldingsscherm via Wayland wordt niet ondersteund"), + ("Reboot required", "Opnieuw opstarten vereist"), + ("Unsupported display server ", "Niet-ondersteunde weergaveserver"), + ("x11 expected", "x11 verwacht"), + ("Port", "Poort"), + ("Settings", "Instellingen"), + ("Username", "Gebruikersnaam"), + ("Invalid port", "Ongeldige poort"), + ("Closed manually by the peer", "Handmatig gesloten door de peer"), + ("Enable remote configuration modification", "Wijziging configuratie op afstand inschakelen"), + ("Run without install", "Uitvoeren zonder installatie"), + ("Always connected via relay", "Altijd verbonden via relay"), + ("Always connect via relay", "Altijd verbinden via relay"), + ("whitelist_tip", "Alleen een IP-adres op de witte lijst krijgt toegang tot mijn toestel"), + ("Login", "Log In"), + ("Verify", "Controleer"), + ("Remember me", "Herinner mij"), + ("Trust this device", "Vertrouw dit apparaat"), + ("Verification code", "Verificatie code"), + ("verification_tip", "Er is een nieuw apparaat gedetecteerd en er is een verificatiecode naar het geregistreerde e-mailadres gestuurd, voer de verificatiecode in om de verbinding voort te zetten."), + ("Logout", "Log Uit"), + ("Tags", "Labels"), + ("Search ID", "Zoek ID"), + ("whitelist_sep", "Gescheiden door komma, puntkomma, spatie of nieuwe regel"), + ("Add ID", "ID Toevoegen"), + ("Add Tag", "Label Toevoegen"), + ("Unselect all tags", "Alle labels verwijderen"), + ("Network error", "Netwerkfout"), + ("Username missed", "Gebruikersnaam gemist"), + ("Password missed", "Wachtwoord vergeten"), + ("Wrong credentials", "Verkeerde inloggegevens"), + ("Edit Tag", "Label Bewerken"), + ("Unremember Password", "Wachtwoord vergeten"), + ("Favorites", "Favorieten"), + ("Add to Favorites", "Toevoegen aan Favorieten"), + ("Remove from Favorites", "Verwijderen uit Favorieten"), + ("Empty", "Leeg"), + ("Invalid folder name", "Ongeldige mapnaam"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Hostname", "Hostnaam"), + ("Discovered", "Ontdekt"), + ("install_daemon_tip", "Om bij het opstarten van de computer te kunnen beginnen, moet je de systeemdienst installeren."), + ("Remote ID", "Externe ID"), + ("Paste", "Plakken"), + ("Paste here?", "Hier plakken"), + ("Are you sure to close the connection?", "Weet je zeker dat je de verbinding wilt sluiten?"), + ("Download new version", "Download nieuwe versie"), + ("Touch mode", "Aanraak modus"), + ("Mouse mode", "Muismodus"), + ("One-Finger Tap", "Een-Vinger Tik"), + ("Left Mouse", "Linkermuis"), + ("One-Long Tap", "Een-Vinger-Lange-Tik"), + ("Two-Finger Tap", "Twee-Vingers-Tik"), + ("Right Mouse", "Rechter muis"), + ("One-Finger Move", "Een-Vinger-Verplaatsing"), + ("Double Tap & Move", "Dubbel Tik en Verplaatsen"), + ("Mouse Drag", "Muis Slepen"), + ("Three-Finger vertically", "Drie-Vinger verticaal"), + ("Mouse Wheel", "Muiswiel"), + ("Two-Finger Move", "Twee-Vingers Verplaatsen"), + ("Canvas Move", "Canvas Verplaatsen"), + ("Pinch to Zoom", "Knijp om te Zoomen"), + ("Canvas Zoom", "Canvas Zoom"), + ("Reset canvas", "Reset canvas"), + ("No permission of file transfer", "Geen toestemming voor bestandsoverdracht"), + ("Note", "Opmerking"), + ("Connection", "Verbinding"), + ("Share Screen", "Scherm Delen"), + ("CLOSE", "SLUITEN"), + ("OPEN", "OPEN"), + ("Chat", "Chat"), + ("Total", "Totaal"), + ("items", "items"), + ("Selected", "Geselecteerd"), + ("Screen Capture", "Schermopname"), + ("Input Control", "Invoercontrole"), + ("Audio Capture", "Audio Opnemen"), + ("File Connection", "Bestandsverbinding"), + ("Screen Connection", "Schermverbinding"), + ("Do you accept?", "Sta je toe?"), + ("Open System Setting", "Systeeminstelling Openen"), + ("How to get Android input permission?", "Hoe krijg ik Android invoer toestemming?"), + ("android_input_permission_tip1", "Om ervoor te zorgen dat een extern apparaat uw Android-apparaat kan besturen via muis of aanraking, moet u RustDesk toestaan om de \"Toegankelijkheid\" service te gebruiken."), + ("android_input_permission_tip2", "Ga naar de volgende pagina met systeeminstellingen, zoek en ga naar [Geinstalleerde Services], schakel de service [RustDesk Input] in."), + ("android_new_connection_tip", "Er is een nieuw controleverzoek binnengekomen, dat uw huidige apparaat wil controleren."), + ("android_service_will_start_tip", "Als u \"Schermopname\" inschakelt, wordt de service automatisch gestart, zodat andere apparaten een verbinding met uw apparaat kunnen aanvragen."), + ("android_stop_service_tip", "Het sluiten van de service zal automatisch alle gemaakte verbindingen sluiten."), + ("android_version_audio_tip", "De huidige versie van Android ondersteunt geen audio-opname, upgrade naar Android 10 of hoger."), + ("android_start_service_tip", "Druk op [Start Service] of op de permissie OPEN [Screenshot] om de service voor het overnemen van het scherm te starten."), + ("Account", "Account"), + ("Overwrite", "Overschrijven"), + ("This file exists, skip or overwrite this file?", "Dit bestand bestaat reeds, overslaan of overschrijven?"), + ("Quit", "Afsluiten"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), + ("Failed", "Mislukt"), + ("Succeeded", "Geslaagd"), + ("Someone turns on privacy mode, exit", "Iemand schakelt privacymodus in, afsluiten"), + ("Unsupported", "Niet Ondersteund"), + ("Peer denied", "Peer geweigerd"), + ("Please install plugins", "Installeer plugins"), + ("Peer exit", "Peer afgesloten"), + ("Failed to turn off", "Uitschakelen mislukt"), + ("Turned off", "Uitgeschakeld"), + ("In privacy mode", "In privacymodus"), + ("Out privacy mode", "Uit privacymodus"), + ("Language", "Taal"), + ("Keep RustDesk background service", "RustDesk achtergronddienst behouden"), + ("Ignore Battery Optimizations", "Negeer Batterij Optimalisaties"), + ("android_open_battery_optimizations_tip", "Ga naar de volgende pagina met instellingen"), + ("Connection not allowed", "Verbinding niet toegestaan"), + ("Legacy mode", "Verouderde modus"), + ("Map mode", "Map mode"), + ("Translate mode", "Vertaalmodus"), + ("Use permanent password", "Gebruik permanent wachtwoord"), + ("Use both passwords", "Gebruik beide wachtwoorden"), + ("Set permanent password", "Stel permanent wachtwoord in"), + ("Enable Remote Restart", "Schakel Herstart op afstand in"), + ("Allow remote restart", "Opnieuw Opstarten op afstand toestaan"), + ("Restart Remote Device", "Apparaat op afstand herstarten"), + ("Are you sure you want to restart", "Weet je zeker dat je wilt herstarten"), + ("Restarting Remote Device", "Apparaat op afstand herstarten"), + ("remote_restarting_tip", "Apparaat op afstand wordt opnieuw opgestart, sluit dit bericht en maak na een ogenblik opnieuw verbinding met het permanente wachtwoord."), + ("Copied", "Gekopieerd"), + ("Exit Fullscreen", "Volledig Scherm sluiten"), + ("Fullscreen", "Volledig Scherm"), + ("Mobile Actions", "Mobiele Acties"), + ("Select Monitor", "Selecteer Monitor"), + ("Control Actions", "Controleacties"), + ("Display Settings", "Beeldscherminstellingen"), + ("Ratio", "Verhouding"), + ("Image Quality", "Beeldkwaliteit"), + ("Scroll Style", "Scroll Stijl"), + ("Show Menubar", "Toon Menubalk"), + ("Hide Menubar", "Verberg Menubalk"), + ("Direct Connection", "Directe Verbinding"), + ("Relay Connection", "Relaisverbinding"), + ("Secure Connection", "Beveiligde Verbinding"), + ("Insecure Connection", "Onveilige Verbinding"), + ("Scale original", "Oorspronkelijke schaal"), + ("Scale adaptive", "Schaalaanpassing"), + ("General", "Algemeen"), + ("Security", "Beveiliging"), + ("Theme", "Thema"), + ("Dark Theme", "Donker Thema"), + ("Dark", "Donker"), + ("Light", "Licht"), + ("Follow System", "Volg Systeem"), + ("Enable hardware codec", "Hardware codec inschakelen"), + ("Unlock Security Settings", "Beveiligingsinstellingen vrijgeven"), + ("Enable Audio", "Audio Inschakelen"), + ("Unlock Network Settings", "Netwerkinstellingen Vrijgeven"), + ("Server", "Server"), + ("Direct IP Access", "Directe IP toegang"), + ("Proxy", "Proxy"), + ("Apply", "Toepassen"), + ("Disconnect all devices?", "Alle apparaten uitschakelen?"), + ("Clear", "Wis"), + ("Audio Input Device", "Audio-invoerapparaat"), + ("Deny remote access", "Toegang op afstand weigeren"), + ("Use IP Whitelisting", "Gebruik een witte lijst van IP-adressen"), + ("Network", "Netwerk"), + ("Enable RDP", "Zet RDP aan"), + ("Pin menubar", "Menubalk Vastzetten"), + ("Unpin menubar", "Menubalk vrijmaken"), + ("Recording", "Opnemen"), + ("Directory", "Map"), + ("Automatically record incoming sessions", "Automatisch inkomende sessies opnemen"), + ("Change", "Wissel"), + ("Start session recording", "Start de sessieopname"), + ("Stop session recording", "Stop de sessieopname"), + ("Enable Recording Session", "Opnamesessie Activeren"), + ("Allow recording session", "Opnamesessie toestaan"), + ("Enable LAN Discovery", "LAN-detectie inschakelen"), + ("Deny LAN Discovery", "LAN-detectie Weigeren"), + ("Write a message", "Schrijf een bericht"), + ("Prompt", "Verzoek"), + ("Please wait for confirmation of UAC...", "Wacht op bevestiging van UAC..."), + ("elevated_foreground_window_tip", "Het momenteel geopende venster van de op afstand bediende computer vereist hogere rechten. Daarom is het momenteel niet mogelijk de muis en het toetsenbord te gebruiken. Vraag de gebruiker wiens computer u op afstand bedient om het venster te minimaliseren of de rechten te verhogen. Om dit probleem in de toekomst te voorkomen, wordt aanbevolen de software te installeren op de op afstand bediende computer."), + ("Disconnected", "Afgesloten"), + ("Other", "Andere"), + ("Confirm before closing multiple tabs", "Bevestig voordat u meerdere tabbladen sluit"), + ("Keyboard Settings", "Toetsenbord instellingen"), + ("Full Access", "Volledige Toegang"), + ("Screen Share", "Scherm Delen"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vereist Ubuntu 21.04 of een hogere versie."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vereist een hogere versie van Linux distro. Probeer X11 desktop of verander je OS."), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Selecteer het scherm dat moet worden gedeeld (Bediening aan de kant van de peer)."), + ("Show RustDesk", "Toon RustDesk"), + ("This PC", "Deze PC"), + ("or", "of"), + ("Continue with", "Ga verder met"), + ("Elevate", "Verhoog"), + ("Zoom cursor", "Cursor Zoomen"), + ("Accept sessions via password", "Sessies accepteren via wachtwoord"), + ("Accept sessions via click", "Sessies accepteren via klik"), + ("Accept sessions via both", "Accepteer sessies via beide"), + ("Please wait for the remote side to accept your session request...", "Wacht tot de andere kant uw sessieverzoek accepteert..."), + ("One-time Password", "Eenmalig Wachtwoord"), + ("Use one-time password", "Gebruik een eenmalig Wachtwoord"), + ("One-time password length", "Eenmalig Wachtwoord lengre"), + ("Request access to your device", "Toegang tot uw toestel aanvragen"), + ("Hide connection management window", "Verberg het venster voor verbindingsbeheer"), + ("hide_cm_tip", "Dit kan alleen als de toegang via een permanent wachtwoord verloopt."), + ("wayland_experiment_tip", "Wayland ondersteuning is slechts experimenteel. Gebruik alsjeblieft X11 als je onbeheerde toegang nodig hebt."), + ("Right click to select tabs", "Rechts klikken om tabbladen te selecteren"), + ("Skipped", "Overgeslagen"), + ("Add to Address Book", "Toevoegen aan Adresboek"), + ("Group", "Groep"), + ("Search", "Zoek"), + ("Closed manually by web console", "Handmatig gesloten door webconsole"), + ("Local keyboard type", "Lokaal toetsenbord"), + ("Select local keyboard type", "Selecteer lokaal toetsenbord"), + ("software_render_tip", "Als u een NVIDIA grafische kaart hebt en het externe venster sluit onmiddellijk na verbinding, kan het helpen om het nieuwe stuurprogramma te installeren en te kiezen voor software rendering. Een software herstart is vereist."), + ("Always use software rendering", "Gebruik altijd software rendering"), + ("config_input", "config_invoer"), + ("config_microphone", "config_microfoon"), + ("request_elevation_tip", "U kunt ook meer rechten vragen als iemand aan de andere kant aanwezig is."), + ("Wait", "Wacht"), + ("Elevation Error", "Verhogingsfout"), + ("Ask the remote user for authentication", "Vraag de gebruiker op afstand om bevestiging"), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", "De gebruiker op afstand moet altijd bevestigen via het UAC-venster van de werkende RustDesk."), + ("Request Elevation", "Verzoek om meer rechten"), + ("wait_accept_uac_tip", "Wacht tot de gebruiker op afstand het UAC-dialoogvenster accepteert."), + ("Elevate successfully", "Succesvolle verhoging van privileges"), + ("uppercase", "Hoofdletter"), + ("lowercase", "kleine letter"), + ("digit", "cijfer"), + ("special character", "speciaal teken"), + ("length>=8", "lengte>=8"), + ("Weak", "Zwak"), + ("Medium", "Midelmatig"), + ("Strong", "Sterk"), + ("Switch Sides", "Wissel van kant"), + ("Please confirm if you want to share your desktop?", "bevestig als je je bureaublad wilt delen?"), + ("Closed as expected", "Gesloten zoals verwacht"), + ("Display", "Weergave"), + ("Default View Style", "Standaard Weergave Stijl"), + ("Default Scroll Style", "Standaard Scroll Stijl"), + ("Default Image Quality", "Standaard Beeldkwaliteit"), + ("Default Codec", "tandaard Codec"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Andere Standaardopties"), + ("Voice call", "Spraakoproep"), + ("Text chat", "Tekst chat"), + ("Stop voice call", "Stop spraakoproep"), + ].iter().cloned().collect(); +} From cea123c79f5f0e39ed394df0f60f0d404949ee27 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 14 Feb 2023 19:20:22 +0800 Subject: [PATCH 055/202] more lang in setup.nsi --- res/setup.nsi | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/res/setup.nsi b/res/setup.nsi index 5410e0ff5..635851d0a 100644 --- a/res/setup.nsi +++ b/res/setup.nsi @@ -56,8 +56,74 @@ InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}" #################################################################### # Language -!insertmacro MUI_LANGUAGE "English" +!insertmacro MUI_LANGUAGE "English" ; The first language is the default language +!insertmacro MUI_LANGUAGE "French" +!insertmacro MUI_LANGUAGE "German" +!insertmacro MUI_LANGUAGE "Spanish" +!insertmacro MUI_LANGUAGE "SpanishInternational" !insertmacro MUI_LANGUAGE "SimpChinese" +!insertmacro MUI_LANGUAGE "TradChinese" +!insertmacro MUI_LANGUAGE "Japanese" +!insertmacro MUI_LANGUAGE "Korean" +!insertmacro MUI_LANGUAGE "Italian" +!insertmacro MUI_LANGUAGE "Dutch" +!insertmacro MUI_LANGUAGE "Danish" +!insertmacro MUI_LANGUAGE "Swedish" +!insertmacro MUI_LANGUAGE "Norwegian" +!insertmacro MUI_LANGUAGE "NorwegianNynorsk" +!insertmacro MUI_LANGUAGE "Finnish" +!insertmacro MUI_LANGUAGE "Greek" +!insertmacro MUI_LANGUAGE "Russian" +!insertmacro MUI_LANGUAGE "Portuguese" +!insertmacro MUI_LANGUAGE "PortugueseBR" +!insertmacro MUI_LANGUAGE "Polish" +!insertmacro MUI_LANGUAGE "Ukrainian" +!insertmacro MUI_LANGUAGE "Czech" +!insertmacro MUI_LANGUAGE "Slovak" +!insertmacro MUI_LANGUAGE "Croatian" +!insertmacro MUI_LANGUAGE "Bulgarian" +!insertmacro MUI_LANGUAGE "Hungarian" +!insertmacro MUI_LANGUAGE "Thai" +!insertmacro MUI_LANGUAGE "Romanian" +!insertmacro MUI_LANGUAGE "Latvian" +!insertmacro MUI_LANGUAGE "Macedonian" +!insertmacro MUI_LANGUAGE "Estonian" +!insertmacro MUI_LANGUAGE "Turkish" +!insertmacro MUI_LANGUAGE "Lithuanian" +!insertmacro MUI_LANGUAGE "Slovenian" +!insertmacro MUI_LANGUAGE "Serbian" +!insertmacro MUI_LANGUAGE "SerbianLatin" +!insertmacro MUI_LANGUAGE "Arabic" +!insertmacro MUI_LANGUAGE "Farsi" +!insertmacro MUI_LANGUAGE "Hebrew" +!insertmacro MUI_LANGUAGE "Indonesian" +!insertmacro MUI_LANGUAGE "Mongolian" +!insertmacro MUI_LANGUAGE "Luxembourgish" +!insertmacro MUI_LANGUAGE "Albanian" +!insertmacro MUI_LANGUAGE "Breton" +!insertmacro MUI_LANGUAGE "Belarusian" +!insertmacro MUI_LANGUAGE "Icelandic" +!insertmacro MUI_LANGUAGE "Malay" +!insertmacro MUI_LANGUAGE "Bosnian" +!insertmacro MUI_LANGUAGE "Kurdish" +!insertmacro MUI_LANGUAGE "Irish" +!insertmacro MUI_LANGUAGE "Uzbek" +!insertmacro MUI_LANGUAGE "Galician" +!insertmacro MUI_LANGUAGE "Afrikaans" +!insertmacro MUI_LANGUAGE "Catalan" +!insertmacro MUI_LANGUAGE "Esperanto" +!insertmacro MUI_LANGUAGE "Asturian" +!insertmacro MUI_LANGUAGE "Basque" +!insertmacro MUI_LANGUAGE "Pashto" +!insertmacro MUI_LANGUAGE "ScotsGaelic" +!insertmacro MUI_LANGUAGE "Georgian" +!insertmacro MUI_LANGUAGE "Vietnamese" +!insertmacro MUI_LANGUAGE "Welsh" +!insertmacro MUI_LANGUAGE "Armenian" +!insertmacro MUI_LANGUAGE "Corsican" +!insertmacro MUI_LANGUAGE "Tatar" +!insertmacro MUI_LANGUAGE "Hindi" + #################################################################### # Sections From d2e0cb396f90cc24ef126da7c0d3766b26ee07f1 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 14 Feb 2023 19:44:14 +0800 Subject: [PATCH 056/202] relay hint msgbox Signed-off-by: 21pages --- flutter/lib/models/model.dart | 42 +++++++++++++++++++++++- src/client.rs | 4 +++ src/client/io_loop.rs | 18 +++++++--- src/flutter_ffi.rs | 7 ++-- src/lang/ca.rs | 3 +- src/lang/cn.rs | 3 +- src/lang/cs.rs | 3 +- src/lang/da.rs | 3 +- src/lang/de.rs | 3 +- src/lang/en.rs | 3 +- src/lang/eo.rs | 3 +- src/lang/es.rs | 3 +- src/lang/fa.rs | 3 +- src/lang/fr.rs | 3 +- src/lang/gr.rs | 3 +- src/lang/hu.rs | 3 +- src/lang/id.rs | 3 +- src/lang/it.rs | 3 +- src/lang/ja.rs | 3 +- src/lang/ko.rs | 3 +- src/lang/kz.rs | 3 +- src/lang/pl.rs | 3 +- src/lang/pt_PT.rs | 3 +- src/lang/ptbr.rs | 3 +- src/lang/ro.rs | 3 +- src/lang/ru.rs | 3 +- src/lang/sk.rs | 3 +- src/lang/sl.rs | 3 +- src/lang/sq.rs | 3 +- src/lang/sr.rs | 3 +- src/lang/sv.rs | 3 +- src/lang/template.rs | 3 +- src/lang/th.rs | 3 +- src/lang/tr.rs | 3 +- src/lang/tw.rs | 3 +- src/lang/ua.rs | 3 +- src/lang/vn.rs | 3 +- src/ui/remote.rs | 6 ++-- src/ui_session_interface.rs | 62 ++++++++++++++++++++++++++++------- 39 files changed, 179 insertions(+), 59 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d0a2ea601..0bd6934a8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -298,6 +298,8 @@ class FfiModel with ChangeNotifier { showWaitUacDialog(id, dialogManager, type); } else if (type == 'elevation-error') { showElevationError(id, type, title, text, dialogManager); + } else if (type == "relay-hint") { + showRelayHintDialog(id, type, title, text, dialogManager); } else { var hasRetry = evt['hasRetry'] == 'true'; showMsgBox(id, type, title, text, link, hasRetry, dialogManager); @@ -312,7 +314,7 @@ class FfiModel with ChangeNotifier { _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { - bind.sessionReconnect(id: id); + bind.sessionReconnect(id: id, forceRelay: false); clearPermissions(); dialogManager.showLoading(translate('Connecting...'), onCancel: closeConnection); @@ -323,6 +325,44 @@ class FfiModel with ChangeNotifier { } } + void showRelayHintDialog(String id, String type, String title, String text, + OverlayDialogManager dialogManager) { + dialogManager.show(tag: '$id-$type', (setState, close) { + onClose() { + closeConnection(); + close(); + } + + reconnect(bool forceRelay) { + bind.sessionReconnect(id: id, forceRelay: forceRelay); + clearPermissions(); + dialogManager.showLoading(translate('Connecting...'), + onCancel: closeConnection); + } + + final style = + ElevatedButton.styleFrom(backgroundColor: Colors.green[700]); + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, + "${translate(text)}\n\n${translate('relay_hint_tip')}"), + actions: [ + dialogButton('Close', onPressed: onClose, isOutline: true), + dialogButton('Retry', onPressed: () => reconnect(false)), + dialogButton('Connect via relay', + onPressed: () => reconnect(true), buttonStyle: style), + dialogButton('Always connect via relay', onPressed: () { + const option = 'force-always-relay'; + bind.sessionPeerOption( + id: id, name: option, value: bool2option(option, true)); + reconnect(true); + }, buttonStyle: style), + ], + onCancel: onClose, + ); + }); + } + /// Handle the peer info event based on [evt]. handlePeerInfo(Map evt, String peerId) async { // recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs) diff --git a/src/client.rs b/src/client.rs index 05b34d781..77221bdb2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -916,6 +916,8 @@ pub struct LoginConfigHandler { pub direct: Option, pub received: bool, switch_uuid: Option, + pub success_time: Option, + pub direct_error_counter: usize, } impl Deref for LoginConfigHandler { @@ -962,6 +964,8 @@ impl LoginConfigHandler { self.direct = None; self.received = false; self.switch_uuid = switch_uuid; + self.success_time = None; + self.direct_error_counter = 0; } /// Check if the client should auto login. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 5186aff4d..de91b091d 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -25,9 +25,8 @@ use hbb_common::{allow_err, get_time, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; use crate::client::{ - new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, - QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, - SERVER_KEYBOARD_ENABLED, + new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, + SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; @@ -148,7 +147,15 @@ impl Remote { Err(err) => { log::error!("Connection closed: {}", err); self.handler.set_force_relay(direct, received); - self.handler.msgbox("error", "Connection Error", &err.to_string(), ""); + let msgtype = "error"; + let title = "Connection Error"; + let text = err.to_string(); + let show_relay_hint = self.handler.show_relay_hint(last_recv_time, msgtype, title, &text); + if show_relay_hint{ + self.handler.msgbox("relay-hint", title, &text, ""); + } else { + self.handler.msgbox(msgtype, title, &text, ""); + } break; } Ok(ref bytes) => { @@ -754,7 +761,8 @@ impl Remote { Data::CloseVoiceCall => { self.stop_voice_call(); let msg = new_voice_call_request(false); - self.handler.on_voice_call_closed("Closed manually by the peer"); + self.handler + .on_voice_call_closed("Closed manually by the peer"); allow_err!(peer.send(&msg).await); } _ => {} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 3025d722c..f8ee512d8 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,4 +1,3 @@ -use crate::ui_session_interface::InvokeUiSession; use crate::{ client::file_trait::FileManager, common::make_fd_to_json, @@ -7,7 +6,7 @@ use crate::{ flutter::{session_add, session_start_}, ui_interface::{self, *}, }; -use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; +use flutter_rust_bridge::{StreamSink, SyncReturn}; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, @@ -157,9 +156,9 @@ pub fn session_record_screen(id: String, start: bool, width: usize, height: usiz } } -pub fn session_reconnect(id: String) { +pub fn session_reconnect(id: String, force_relay: bool) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.reconnect(); + session.reconnect(force_relay); } } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e98c6636a..d483a185d 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Tancat manualment pel peer"), ("Enable remote configuration modification", "Habilitar modificació remota de configuració"), ("Run without install", "Executar sense instal·lar"), - ("Always connected via relay", "Connectat sempre a través de relay"), + ("Connect via relay", ""), ("Always connect via relay", "Connecta sempre a través de relay"), ("whitelist_tip", ""), ("Login", "Inicia sessió"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 64c37709a..7dea516ba 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "被对方手动关闭"), ("Enable remote configuration modification", "允许远程修改配置"), ("Run without install", "无安装运行"), - ("Always connected via relay", "强制走中继连接"), + ("Connect via relay", "中继连接"), ("Always connect via relay", "强制走中继连接"), ("whitelist_tip", "只有白名单里的ip才能访问我"), ("Login", "登录"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "语音通话"), ("Text chat", "文字聊天"), ("Stop voice call", "停止语音聊天"), + ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在ID后面添加/r,或者在卡片选项里选择强制走中继连接。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 70a3eb6c7..97a3ebc48 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Ručně ukončeno protějškem"), ("Enable remote configuration modification", "Umožnit upravování nastavení vzdáleného"), ("Run without install", "Spustit bez instalování"), - ("Always connected via relay", "Vždy spojováno prostřednictvím brány pro předávání (relay)"), + ("Connect via relay", ""), ("Always connect via relay", "Vždy se spojovat prostřednictvím brány pro předávání (relay)"), ("whitelist_tip", "Přístup je umožněn pouze z IP adres, nacházejících se na seznamu povolených"), ("Login", "Přihlásit se"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index ae943e1e8..bab81914e 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Manuelt lukket af peer"), ("Enable remote configuration modification", "Tillad at ændre afstandskonfigurationen"), ("Run without install", "Kør uden installation"), - ("Always connected via relay", "Tilslut altid via relæ-server"), + ("Connect via relay", ""), ("Always connect via relay", "Forbindelse via relæ-server"), ("whitelist_tip", "Kun IP'er på udgivelseslisten kan få adgang til mig"), ("Login", "Login"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 1743505cc..05d02dd58 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen"), ("Enable remote configuration modification", "Änderung der Konfiguration aus der Ferne zulassen"), ("Run without install", "Ohne Installation ausführen"), - ("Always connected via relay", "Immer über Relay-Server verbunden"), + ("Connect via relay", ""), ("Always connect via relay", "Immer über Relay-Server verbinden"), ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Sprachanruf"), ("Text chat", "Text-Chat"), ("Stop voice call", "Sprachanruf beenden"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 37c08a974..4bfa86349 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -42,6 +42,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("request_elevation_tip","You can also request elevation if there is someone on the remote side."), ("wait_accept_uac_tip","Please wait for the remote user to accept the UAC dialog."), ("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk."), - ("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions.") + ("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions."), + ("relay_hint_tip", "It may not be possible to connect directly, you can try to connect via relay. \nIn addition, if you want to use relay on your first try, you can add the \"/r\" suffix to the ID, or select the option \"Always connect via relay\" in the peer card."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index f457833f8..47eeb3367 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Manuale fermita de la samtavolano"), ("Enable remote configuration modification", "Permesi foran redaktadon de la konfiguracio"), ("Run without install", "Plenumi sen instali"), - ("Always connected via relay", "Ĉiam konektata per relajso"), + ("Connect via relay", ""), ("Always connect via relay", "Ĉiam konekti per relajso"), ("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"), ("Login", "Konekti"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 939a4831f..4634cea81 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Cerrado manualmente por el par"), ("Enable remote configuration modification", "Habilitar modificación remota de configuración"), ("Run without install", "Ejecutar sin instalar"), - ("Always connected via relay", "Siempre conectado a través de relay"), + ("Connect via relay", ""), ("Always connect via relay", "Conéctese siempre a través de relay"), ("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"), ("Login", "Iniciar sesión"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Llamada de voz"), ("Text chat", "Chat de texto"), ("Stop voice call", "Detener llamada de voz"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 8413673a1..2d0f29a5b 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "به صورت دستی توسط میزبان بسته شد"), ("Enable remote configuration modification", "فعال بودن اعمال تغییرات پیکربندی از راه دور"), ("Run without install", "بدون نصب اجرا شود"), - ("Always connected via relay", "متصل است Relay همیشه با"), + ("Connect via relay", ""), ("Always connect via relay", "برای اتصال استفاده شود Relay از"), ("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"), ("Login", "ورود"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "تماس صوتی"), ("Text chat", "گفتگو متنی (چت متنی)"), ("Stop voice call", "توقف تماس صوتی"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 39ee3bc7f..4e0e79aa0 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Fermé manuellement par le pair"), ("Enable remote configuration modification", "Autoriser la modification de la configuration à distance"), ("Run without install", "Exécuter sans installer"), - ("Always connected via relay", "Forcer la connexion relais"), + ("Connect via relay", ""), ("Always connect via relay", "Forcer la connexion relais"), ("whitelist_tip", "Seule une IP de la liste blanche peut accéder à mon appareil"), ("Login", "Connexion"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 7cb678ecc..09284738a 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Έκλεισε από τον απομακρυσμένο σταθμό"), ("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης ρυθμίσεων"), ("Run without install", "Εκτέλεση χωρίς εγκατάσταση"), - ("Always connected via relay", "Πάντα συνδεδεμένο μέσω αναμετάδοσης"), + ("Connect via relay", ""), ("Always connect via relay", "Σύνδεση πάντα μέσω αναμετάδοσης"), ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"), ("Login", "Σύνδεση"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 25562f556..16c99d207 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "A kapcsolatot a másik fél manuálisan bezárta"), ("Enable remote configuration modification", "Távoli konfiguráció módosítás engedélyezése"), ("Run without install", "Futtatás feltelepítés nélkül"), - ("Always connected via relay", "Mindig közvetítőn keresztül csatlakozik"), + ("Connect via relay", ""), ("Always connect via relay", "Mindig közvetítőn keresztüli csatlakozás"), ("whitelist_tip", "Csak az engedélyezési listán szereplő címek csatlakozhatnak"), ("Login", "Belépés"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 68a80e540..f4be0396f 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Ditutup secara manual oleh peer"), ("Enable remote configuration modification", "Aktifkan modifikasi konfigurasi jarak jauh"), ("Run without install", "Jalankan tanpa menginstal"), - ("Always connected via relay", "Selalu terhubung melalui relai"), + ("Connect via relay", ""), ("Always connect via relay", "Selalu terhubung melalui relai"), ("whitelist_tip", "Hanya whitelisted IP yang dapat mengakses saya"), ("Login", "Masuk"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index a4ea58304..15f7b977f 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Chiuso manualmente dal peer"), ("Enable remote configuration modification", "Abilita la modifica remota della configurazione"), ("Run without install", "Esegui senza installare"), - ("Always connected via relay", "Connesso sempre tramite relay"), + ("Connect via relay", ""), ("Always connect via relay", "Collegati sempre tramite relay"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("Login", "Accedi"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Chiamata vocale"), ("Text chat", "Chat testuale"), ("Stop voice call", "Interrompi la chiamata vocale"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 7069c0daf..acf1c9b96 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "相手が手動で切断しました"), ("Enable remote configuration modification", "リモート設定変更を有効化"), ("Run without install", "インストールせずに実行"), - ("Always connected via relay", "常に中継サーバー経由で接続"), + ("Connect via relay", ""), ("Always connect via relay", "常に中継サーバー経由で接続"), ("whitelist_tip", "ホワイトリストに登録されたIPからのみ接続を許可します"), ("Login", "ログイン"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 43eb552d3..e1bc43182 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "다른 사용자에 의해 종료됨"), ("Enable remote configuration modification", "원격 구성 변경 활성화"), ("Run without install", "설치 없이 실행"), - ("Always connected via relay", "항상 relay를 통해 접속됨"), + ("Connect via relay", ""), ("Always connect via relay", "항상 relay를 통해 접속하기"), ("whitelist_tip", "화이트리스트에 있는 IP만 현 데스크탑에 접속 가능합니다"), ("Login", "로그인"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 49c7b9916..488290537 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Пир қолымен жабылған"), ("Enable remote configuration modification", "Қашықтан қалыптарды өзгертуді іске қосу"), ("Run without install", "Орнатпай-ақ Іске қосу"), - ("Always connected via relay", "Әрқашан да релай сербері арқылы қосулы"), + ("Connect via relay", ""), ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), ("Login", "Кіру"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 41239961a..e6ba5b171 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Połączenie zakończone ręcznie przez peer"), ("Enable remote configuration modification", "Włącz zdalną modyfikację konfiguracji"), ("Run without install", "Uruchom bez instalacji"), - ("Always connected via relay", "Zawsze połączony pośrednio"), + ("Connect via relay", ""), ("Always connect via relay", "Zawsze łącz pośrednio"), ("whitelist_tip", "Zezwalaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), ("Login", "Zaloguj"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e69a140c9..a1ad932b1 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Fechada manualmente pelo destino"), ("Enable remote configuration modification", "Habilitar modificações de configuração remotas"), ("Run without install", "Executar sem instalar"), - ("Always connected via relay", "Sempre conectado via relay"), + ("Connect via relay", ""), ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs na whitelist podem me acessar"), ("Login", "Login"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 0887a5915..5ece46006 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Fechada manualmente pelo parceiro"), ("Enable remote configuration modification", "Habilitar modificações de configuração remotas"), ("Run without install", "Executar sem instalar"), - ("Always connected via relay", "Sempre conectado via relay"), + ("Connect via relay", ""), ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs confiáveis podem me acessar"), ("Login", "Login"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 304353d42..e9b83e298 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Închis manual de dispozitivul pereche"), ("Enable remote configuration modification", "Activează modificarea configurației de la distanță"), ("Run without install", "Rulează fără instalare"), - ("Always connected via relay", "Se conectează mereu prin retransmisie"), + ("Connect via relay", ""), ("Always connect via relay", "Se conectează mereu prin retransmisie"), ("whitelist_tip", "Doar adresele IP autorizate pot accesa acest dispozitiv"), ("Login", "Conectare"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 1792eccce..a8ef18d8a 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Закрыто удалённым узлом вручную"), ("Enable remote configuration modification", "Разрешить удалённое изменение конфигурации"), ("Run without install", "Запустить без установки"), - ("Always connected via relay", "Всегда подключается через ретрансляционный сервер"), + ("Connect via relay", ""), ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("Login", "Войти"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Голосовой вызов"), ("Text chat", "Текстовый чат"), ("Stop voice call", "Завершить голосовой вызов"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 6f6f7a18e..47a795342 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Manuálne ukončené opačnou stranou pripojenia"), ("Enable remote configuration modification", "Povoliť zmeny konfigurácie zo vzdialeného PC"), ("Run without install", "Spustiť bez inštalácie"), - ("Always connected via relay", "Vždy pripojené cez prepájací server"), + ("Connect via relay", ""), ("Always connect via relay", "Vždy pripájať cez prepájací server"), ("whitelist_tip", "Len vymenované IP adresy majú oprávnenie sa pripojiť k vzdialenej správe"), ("Login", "Prihlásenie"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 2fb74fa5d..1eb33b970 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Povezavo ročno prekinil odjemalec"), ("Enable remote configuration modification", "Omogoči oddaljeno spreminjanje nastavitev"), ("Run without install", "Zaženi brez namestitve"), - ("Always connected via relay", "Vedno povezan preko posrednika"), + ("Connect via relay", ""), ("Always connect via relay", "Vedno poveži preko posrednika"), ("whitelist_tip", "Dostop je možen samo iz dovoljenih IPjev"), ("Login", "Prijavi"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 5d4a6e1ad..1ade9757a 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "E mbyllur manualisht nga peer"), ("Enable remote configuration modification", "Aktivizoni modifikimin e konfigurimit në distancë"), ("Run without install", "Ekzekuto pa instaluar"), - ("Always connected via relay", "Gjithmonë i ldihur me transmetues"), + ("Connect via relay", ""), ("Always connect via relay", "Gjithmonë lidheni me transmetues"), ("whitelist_tip", "Vetëm IP e listës së bardhë mund të më aksesoj."), ("Login", "Hyrje"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 31a3ade8f..e5704093d 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Klijent ručno raskinuo konekciju"), ("Enable remote configuration modification", "Dozvoli modifikaciju udaljene konfiguracije"), ("Run without install", "Pokreni bez instalacije"), - ("Always connected via relay", "Uvek spojne preko posrednika"), + ("Connect via relay", ""), ("Always connect via relay", "Uvek se spoj preko posrednika"), ("whitelist_tip", "Samo dozvoljene IP mi mogu pristupiti"), ("Login", "Prijava"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index e30c09e44..063892074 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Stängd manuellt av klienten"), ("Enable remote configuration modification", "Tillåt fjärrkonfigurering"), ("Run without install", "Kör utan installation"), - ("Always connected via relay", "Anslut alltid via relay"), + ("Connect via relay", ""), ("Always connect via relay", "Anslut alltid via relay"), ("whitelist_tip", "Bara vitlistade IPs kan koppla upp till mig"), ("Login", "Logga in"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b88618074..4190ba399 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", ""), ("Enable remote configuration modification", ""), ("Run without install", ""), - ("Always connected via relay", ""), + ("Connect via relay", ""), ("Always connect via relay", ""), ("whitelist_tip", ""), ("Login", ""), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 1c75aaae7..629c5ac77 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "ถูกปิดโดยอีกฝั่งการการเชื่อมต่อ"), ("Enable remote configuration modification", "เปิดการใช้งานการแก้ไขการตั้งค่าปลายทาง"), ("Run without install", "ใช้งานโดยไม่ต้องติดตั้ง"), - ("Always connected via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), + ("Connect via relay", ""), ("Always connect via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), ("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"), ("Login", "เข้าสู่ระบบ"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index a9e2c1715..b683fb78a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Eş tarafından manuel olarak kapatıldı"), ("Enable remote configuration modification", "Uzaktan yapılandırma değişikliğini etkinleştir"), ("Run without install", "Yüklemeden çalıştır"), - ("Always connected via relay", "Her zaman röle ile bağlı"), + ("Connect via relay", ""), ("Always connect via relay", "Always connect via relay"), ("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"), ("Login", "Giriş yap"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 7c49a29a2..e4957e3d7 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "由對方手動關閉"), ("Enable remote configuration modification", "啟用遠端更改設定"), ("Run without install", "跳過安裝直接執行"), - ("Always connected via relay", "一律透過轉送連線"), + ("Connect via relay", ""), ("Always connect via relay", "一律透過轉送連線"), ("whitelist_tip", "只有白名單中的 IP 可以存取"), ("Login", "登入"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 92c99d90c..3c1d7776a 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Закрито вузлом вручну"), ("Enable remote configuration modification", "Дозволити віддалену зміну конфігурації"), ("Run without install", "Запустити без установки"), - ("Always connected via relay", "Завжди підключений через ретрансляційний сервер"), + ("Connect via relay", ""), ("Always connect via relay", "Завжди підключатися через ретрансляційний сервер"), ("whitelist_tip", "Тільки IP-адреси з білого списку можуть отримати доступ до мене"), ("Login", "Увійти"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 8bb1d45e9..76f611429 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Đóng thủ công bởi peer"), ("Enable remote configuration modification", "Cho phép thay đổi cấu hình bên từ xa"), ("Run without install", "Chạy mà không cần cài"), - ("Always connected via relay", "Luôn đuợc kết nối qua relay"), + ("Connect via relay", ""), ("Always connect via relay", "Luôn kết nối qua relay"), ("whitelist_tip", "Chỉ có những IP đựoc cho phép mới có thể truy cập"), ("Login", "Đăng nhập"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 447c2e31d..1725a8f41 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,4 +1,3 @@ -use std::sync::RwLock; use std::{ collections::HashMap, ops::{Deref, DerefMut}, @@ -15,7 +14,6 @@ use sciter::{ Value, }; -use hbb_common::tokio::io::AsyncReadExt; use hbb_common::{ allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType, }; @@ -348,7 +346,7 @@ impl sciter::EventHandler for SciterSession { let site = AssetPtr::adopt(ptr as *mut video_destination); log::debug!("[video] start video"); *VIDEO.lock().unwrap() = Some(site); - self.reconnect(); + self.reconnect(false); } } BEHAVIOR_EVENTS::VIDEO_INITIALIZED => { @@ -397,7 +395,7 @@ impl sciter::EventHandler for SciterSession { fn transfer_file(); fn tunnel(); fn lock_screen(); - fn reconnect(); + fn reconnect(bool); fn get_chatbox(); fn get_icon(); fn get_home_dir(); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 25c15f52f..97db904d4 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,29 +1,30 @@ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use std::sync::{Arc, Mutex, RwLock}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::Duration; use async_trait::async_trait; use bytes::Bytes; use rdev::{Event, EventType::*}; use uuid::Uuid; -use hbb_common::{allow_err, message_proto::*}; -use hbb_common::{fs, get_version_number, log, Stream}; use hbb_common::config::{Config, LocalConfig, PeerConfig, RS_PUB_KEY}; use hbb_common::rendezvous_proto::ConnType; use hbb_common::tokio::{self, sync::mpsc}; +use hbb_common::{allow_err, message_proto::*}; +use hbb_common::{fs, get_version_number, log, Stream}; -use crate::{client::Data, client::Interface}; -use crate::client::{ - check_if_retry, FileManager, handle_hash, handle_login_error, handle_login_from_ui, - handle_test_delay, input_os_password, Key, KEY_MAP, load_config, LoginConfigHandler, - QualityStatus, send_mouse, start_video_audio_threads, -}; use crate::client::io_loop::Remote; +use crate::client::{ + check_if_retry, handle_hash, handle_login_error, handle_login_from_ui, handle_test_delay, + input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, + LoginConfigHandler, QualityStatus, KEY_MAP, +}; use crate::common::{self, GrabState}; use crate::keyboard; +use crate::{client::Data, client::Interface}; pub static IS_IN: AtomicBool = AtomicBool::new(false); @@ -531,9 +532,13 @@ impl Session { } } - pub fn reconnect(&self) { + pub fn reconnect(&self, force_relay: bool) { self.send(Data::Close); let cloned = self.clone(); + // override only if true + if true == force_relay { + cloned.lc.write().unwrap().force_relay = true; + } let mut lock = self.thread.lock().unwrap(); lock.take().map(|t| t.join()); *lock = Some(std::thread::spawn(move || { @@ -674,10 +679,42 @@ impl Session { pub fn request_voice_call(&self) { self.send(Data::NewVoiceCall); } - + pub fn close_voice_call(&self) { self.send(Data::CloseVoiceCall); } + + pub fn show_relay_hint( + &mut self, + last_recv_time: tokio::time::Instant, + msgtype: &str, + title: &str, + text: &str, + ) -> bool { + let duration = Duration::from_secs(3); + let counter_interval = 3; + let lock = self.lc.read().unwrap(); + let success_time = lock.success_time; + let direct = lock.direct.unwrap_or(false); + let received = lock.received; + drop(lock); + if let Some(success_time) = success_time { + if direct && last_recv_time.duration_since(success_time) < duration { + let retry_for_relay = direct && !received; + let retry = check_if_retry(msgtype, title, text, retry_for_relay); + if retry && !retry_for_relay { + self.lc.write().unwrap().direct_error_counter += 1; + if self.lc.read().unwrap().direct_error_counter % counter_interval == 0 { + #[cfg(feature = "flutter")] + return true; + } + } + } else { + self.lc.write().unwrap().direct_error_counter = 0; + } + } + false + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { @@ -813,6 +850,7 @@ impl Interface for Session { "Connected, waiting for image...", "", ); + self.lc.write().unwrap().success_time = Some(tokio::time::Instant::now()); } self.on_connected(self.lc.read().unwrap().conn_type); #[cfg(windows)] @@ -958,7 +996,7 @@ pub async fn io_loop(handler: Session) { let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_cl = frame_count.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: &mut Vec | { + let (video_sender, audio_sender) = start_video_audio_threads(move |data: &mut Vec| { frame_count_cl.fetch_add(1, Ordering::Relaxed); ui_handler.on_rgba(data); }); From 491317bd6fe5cd931e61215a0c40e101a705054b Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Tue, 14 Feb 2023 13:57:33 +0100 Subject: [PATCH 057/202] modernized menu bar --- flutter/assets/actions.svg | 3 + flutter/assets/chat.svg | 3 +- flutter/assets/close.svg | 2 + flutter/assets/display.svg | 2 + flutter/assets/fullscreen.svg | 2 + flutter/assets/fullscreen_exit.svg | 2 + flutter/assets/keyboard.svg | 2 + flutter/assets/pinned.svg | 2 + flutter/assets/rec.svg | 2 + flutter/assets/unpinned.svg | 2 + .../lib/desktop/widgets/remote_menubar.dart | 227 +++++++++--------- 11 files changed, 138 insertions(+), 111 deletions(-) create mode 100644 flutter/assets/actions.svg create mode 100644 flutter/assets/close.svg create mode 100644 flutter/assets/display.svg create mode 100644 flutter/assets/fullscreen.svg create mode 100644 flutter/assets/fullscreen_exit.svg create mode 100644 flutter/assets/keyboard.svg create mode 100644 flutter/assets/pinned.svg create mode 100644 flutter/assets/rec.svg create mode 100644 flutter/assets/unpinned.svg diff --git a/flutter/assets/actions.svg b/flutter/assets/actions.svg new file mode 100644 index 000000000..feaf416cd --- /dev/null +++ b/flutter/assets/actions.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg index 03491be6e..830ef0d33 100644 --- a/flutter/assets/chat.svg +++ b/flutter/assets/chat.svg @@ -1 +1,2 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/flutter/assets/close.svg b/flutter/assets/close.svg new file mode 100644 index 000000000..1e9a30711 --- /dev/null +++ b/flutter/assets/close.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/display.svg b/flutter/assets/display.svg new file mode 100644 index 000000000..8a87116ff --- /dev/null +++ b/flutter/assets/display.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/fullscreen.svg b/flutter/assets/fullscreen.svg new file mode 100644 index 000000000..73d79cf0e --- /dev/null +++ b/flutter/assets/fullscreen.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/fullscreen_exit.svg b/flutter/assets/fullscreen_exit.svg new file mode 100644 index 000000000..f2b3ae27b --- /dev/null +++ b/flutter/assets/fullscreen_exit.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg new file mode 100644 index 000000000..569c68727 --- /dev/null +++ b/flutter/assets/keyboard.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/pinned.svg b/flutter/assets/pinned.svg new file mode 100644 index 000000000..2563015f7 --- /dev/null +++ b/flutter/assets/pinned.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/rec.svg b/flutter/assets/rec.svg new file mode 100644 index 000000000..14546b971 --- /dev/null +++ b/flutter/assets/rec.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/unpinned.svg b/flutter/assets/unpinned.svg new file mode 100644 index 000000000..ba4ab5328 --- /dev/null +++ b/flutter/assets/unpinned.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6bb49000b..77d687d93 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -405,9 +405,10 @@ class _RemoteMenubarState extends State { Widget _buildMenubar(BuildContext context) { final List menubarItems = []; + final double iconSize = Theme.of(context).iconTheme.size ?? 30.0; if (!isWebDesktop) { - menubarItems.add(_buildPinMenubar(context)); - menubarItems.add(_buildFullscreen(context)); + menubarItems.add(_buildPinMenubar(context, iconSize)); + menubarItems.add(_buildFullscreen(context, iconSize)); if (widget.ffi.ffiModel.isPeerAndroid) { menubarItems.add(IconButton( tooltip: translate('Mobile Actions'), @@ -420,77 +421,84 @@ class _RemoteMenubarState extends State { )); } } - menubarItems.add(_buildMonitor(context)); - menubarItems.add(_buildControl(context)); - menubarItems.add(_buildDisplay(context)); - menubarItems.add(_buildKeyboard(context)); + menubarItems.add(_buildMonitor(context, iconSize)); + menubarItems.add(_buildControl(context, iconSize)); + menubarItems.add(_buildDisplay(context, iconSize)); + menubarItems.add(_buildKeyboard(context, iconSize)); if (!isWeb) { - menubarItems.add(_buildChat(context)); - menubarItems.add(_buildVoiceCall(context)); + menubarItems.add(_buildChat(context, iconSize)); + menubarItems.add(_buildVoiceCall(context, iconSize)); } - menubarItems.add(_buildRecording(context)); - menubarItems.add(_buildClose(context)); + menubarItems.add(_buildRecording(context, iconSize)); + menubarItems.add(_buildClose(context, iconSize)); return PopupMenuTheme( - data: const PopupMenuThemeData( - textStyle: TextStyle(color: _MenubarTheme.commonColor)), - child: Column(mainAxisSize: MainAxisSize.min, children: [ + data: const PopupMenuThemeData( + textStyle: TextStyle(color: _MenubarTheme.commonColor)), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: MyTheme.border), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(10), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: menubarItems, - )), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: menubarItems, + ), + ), _buildDraggableShowHide(context), - ])); + ], + ), + ); } - Widget _buildPinMenubar(BuildContext context) { - return Obx(() => IconButton( - tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), - onPressed: () { - widget.state.switchPin(); - }, - icon: Obx(() => Transform.rotate( - angle: pin ? math.pi / 4 : 0, - child: Icon( - Icons.push_pin, - color: pin ? _MenubarTheme.commonColor : Colors.grey, - ))), - )); + Widget _buildPinMenubar(BuildContext context, double iconSize) { + return Obx( + () => IconButton( + padding: EdgeInsets.zero, + iconSize: iconSize, + tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), + onPressed: () { + widget.state.switchPin(); + }, + icon: SvgPicture.asset( + pin ? "assets/pinned.svg" : "assets/unpinned.svg", + color: pin ? _MenubarTheme.commonColor : Colors.grey[800], + ), + ), + ); } - Widget _buildFullscreen(BuildContext context) { + Widget _buildFullscreen(BuildContext context, double iconSize) { return IconButton( + padding: EdgeInsets.zero, + iconSize: iconSize, tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), onPressed: () { _setFullscreen(!isFullscreen); }, - icon: isFullscreen - ? const Icon( - Icons.fullscreen_exit, - color: _MenubarTheme.commonColor, - ) - : const Icon( - Icons.fullscreen, - color: _MenubarTheme.commonColor, - ), + icon: SvgPicture.asset( + isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", + color: _MenubarTheme.commonColor, + ), ); } - Widget _buildMonitor(BuildContext context) { + Widget _buildMonitor(BuildContext context, double iconSize) { final pi = widget.ffi.ffiModel.pi; return mod_menu.PopupMenuButton( + iconSize: iconSize, tooltip: translate('Select Monitor'), padding: EdgeInsets.zero, position: mod_menu.PopupMenuPosition.under, icon: Stack( alignment: Alignment.center, children: [ - const Icon( - Icons.personal_video, + SvgPicture.asset( + "assets/display.svg", color: _MenubarTheme.commonColor, ), Padding( @@ -499,8 +507,7 @@ class _RemoteMenubarState extends State { RxInt display = CurrentDisplayState.find(widget.id); return Text( '${display.value + 1}/${pi.displays.length}', - style: const TextStyle( - color: _MenubarTheme.commonColor, fontSize: 8), + style: const TextStyle(color: Colors.white, fontSize: 8), ); }), ) @@ -513,23 +520,22 @@ class _RemoteMenubarState extends State { Stack( alignment: Alignment.center, children: [ - const Icon( - Icons.personal_video, - color: _MenubarTheme.commonColor, - ), + SvgPicture.asset("assets/display.svg"), TextButton( child: Container( - alignment: AlignmentDirectional.center, - constraints: - const BoxConstraints(minHeight: _MenubarTheme.height), - child: Padding( - padding: const EdgeInsets.only(bottom: 2.5), - child: Text( - (i + 1).toString(), - style: - const TextStyle(color: _MenubarTheme.commonColor), + alignment: AlignmentDirectional.center, + constraints: + const BoxConstraints(minHeight: _MenubarTheme.height), + child: Padding( + padding: const EdgeInsets.only(bottom: 2.5), + child: Text( + (i + 1).toString(), + style: TextStyle( + color: Theme.of(context).scaffoldBackgroundColor, ), - )), + ), + ), + ), onPressed: () { if (Navigator.canPop(context)) { Navigator.pop(context); @@ -561,11 +567,12 @@ class _RemoteMenubarState extends State { ); } - Widget _buildControl(BuildContext context) { + Widget _buildControl(BuildContext context, double iconSize) { return mod_menu.PopupMenuButton( + iconSize: iconSize, padding: EdgeInsets.zero, - icon: const Icon( - Icons.bolt, + icon: SvgPicture.asset( + "assets/actions.svg", color: _MenubarTheme.commonColor, ), tooltip: translate('Control Actions'), @@ -583,7 +590,7 @@ class _RemoteMenubarState extends State { ); } - Widget _buildDisplay(BuildContext context) { + Widget _buildDisplay(BuildContext context, double iconSize) { return FutureBuilder(future: () async { widget.state.viewStyle.value = await bind.sessionGetViewStyle(id: widget.id) ?? ''; @@ -595,9 +602,10 @@ class _RemoteMenubarState extends State { return Obx(() { final remoteCount = RemoteCountState.find().value; return mod_menu.PopupMenuButton( + iconSize: iconSize, padding: EdgeInsets.zero, - icon: const Icon( - Icons.tv, + icon: SvgPicture.asset( + "assets/display.svg", color: _MenubarTheme.commonColor, ), tooltip: translate('Display Settings'), @@ -622,15 +630,16 @@ class _RemoteMenubarState extends State { }); } - Widget _buildKeyboard(BuildContext context) { + Widget _buildKeyboard(BuildContext context, double iconSize) { FfiModel ffiModel = Provider.of(context); if (ffiModel.permissions['keyboard'] == false) { return Offstage(); } return mod_menu.PopupMenuButton( + iconSize: iconSize, padding: EdgeInsets.zero, - icon: const Icon( - Icons.keyboard, + icon: SvgPicture.asset( + "assets/keyboard.svg", color: _MenubarTheme.commonColor, ), tooltip: translate('Keyboard Settings'), @@ -648,57 +657,54 @@ class _RemoteMenubarState extends State { ); } - Widget _buildRecording(BuildContext context) { + Widget _buildRecording(BuildContext context, double iconSize) { return Consumer(builder: ((context, value, child) { if (value.permissions['recording'] != false) { return Consumer( - builder: (context, value, child) => IconButton( - tooltip: value.start - ? translate('Stop session recording') - : translate('Start session recording'), - onPressed: () => value.toggle(), - icon: value.start - ? Icon( - Icons.pause_circle_filled, - color: _MenubarTheme.commonColor, - ) - : SvgPicture.asset( - "assets/record_screen.svg", - color: _MenubarTheme.commonColor, - width: Theme.of(context).iconTheme.size ?? 22.0, - height: Theme.of(context).iconTheme.size ?? 22.0, - ), - )); + builder: (context, value, child) => IconButton( + padding: EdgeInsets.zero, + iconSize: iconSize, + tooltip: value.start + ? translate('Stop session recording') + : translate('Start session recording'), + onPressed: () => value.toggle(), + icon: SvgPicture.asset( + "assets/rec.svg", + color: value.start ? Colors.red : _MenubarTheme.commonColor, + ), + ), + ); } else { return Offstage(); } })); } - Widget _buildClose(BuildContext context) { + Widget _buildClose(BuildContext context, double iconSize) { return IconButton( + iconSize: iconSize, + padding: EdgeInsets.zero, tooltip: translate('Close'), onPressed: () { clientClose(widget.id, widget.ffi.dialogManager); }, - icon: const Icon( - Icons.close, - color: _MenubarTheme.commonColor, + icon: SvgPicture.asset( + "assets/close.svg", + color: Colors.red, ), ); } final _chatButtonKey = GlobalKey(); - Widget _buildChat(BuildContext context) { + Widget _buildChat(BuildContext context, double iconSize) { FfiModel ffiModel = Provider.of(context); return mod_menu.PopupMenuButton( + iconSize: iconSize, key: _chatButtonKey, padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/chat.svg", color: _MenubarTheme.commonColor, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, ), tooltip: translate('Chat'), position: mod_menu.PopupMenuPosition.under, @@ -719,15 +725,14 @@ class _RemoteMenubarState extends State { switch (widget.ffi.chatModel.voiceCallStatus.value) { case VoiceCallStatus.waitingForResponse: return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: SvgPicture.asset( - "assets/voice_call_waiting.svg", - color: Colors.red, - width: Theme.of(context).iconTheme.size ?? 20.0, - height: Theme.of(context).iconTheme.size ?? 20.0, - )); + onPressed: () { + widget.ffi.chatModel.closeVoiceCall(widget.id); + }, + icon: SvgPicture.asset( + "assets/voice_call_waiting.svg", + color: Colors.red, + ), + ); case VoiceCallStatus.connected: return IconButton( onPressed: () { @@ -736,7 +741,6 @@ class _RemoteMenubarState extends State { icon: Icon( Icons.phone_disabled_rounded, color: Colors.red, - size: Theme.of(context).iconTheme.size ?? 22.0, ), ); default: @@ -755,13 +759,14 @@ class _RemoteMenubarState extends State { } } - Widget _buildVoiceCall(BuildContext context) { + Widget _buildVoiceCall(BuildContext context, double iconSize) { return Obx( () { final tooltipText = _getVoiceCallTooltip(); return tooltipText == null ? const Offstage() : IconButton( + iconSize: iconSize, padding: EdgeInsets.zero, icon: _getVoiceCallIcon(), tooltip: translate(tooltipText), @@ -1748,7 +1753,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { child: Icon( Icons.drag_indicator, size: 20, - color: Colors.grey, + color: Colors.grey[800], ), feedback: widget, onDragStarted: (() { @@ -1801,7 +1806,9 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { child: Container( decoration: BoxDecoration( color: Colors.white, - border: Border.all(color: MyTheme.border), + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(5), + ), ), child: SizedBox( height: 20, From 50f751c21521fd63985a9123f05bb706c048ba37 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 10 Feb 2023 09:03:19 +0800 Subject: [PATCH 058/202] temp commit Signed-off-by: fufesou --- src/keyboard.rs | 10 ++++++---- src/server/input_service.rs | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 105b84400..9ca5a16f0 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -759,10 +759,12 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option) { match &event.unicode { Some(unicode_info) => { - for code in &unicode_info.unicode { - let mut evt = key_event.clone(); - evt.set_unicode(*code as _); - events.push(evt); + if let Some(name) = unicode_info.name { + if name.len() > 0 { + let mut evt = key_event.clone(); + evt.set_seq(name); + events.push(evt); + } } } None => {} diff --git a/src/server/input_service.rs b/src/server/input_service.rs index edf0ef497..2b19bbaff 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1093,6 +1093,9 @@ fn translate_keyboard_mode(evt: &KeyEvent) { #[cfg(target_os = "windows")] allow_err!(rdev::simulate_unicode(_unicode as _)); } + Some(key_event::Union::Seq(seq)) => { + ENIGO.lock().unwrap().key_sequence(&seq); + } Some(key_event::Union::Chr(..)) => { #[cfg(target_os = "windows")] From e24f5e7eed10b321500fb6fdfe64d7e8bb766d87 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 13 Feb 2023 14:55:57 +0800 Subject: [PATCH 059/202] mid commit Signed-off-by: fufesou --- libs/hbb_common/protos/message.proto | 2 ++ src/keyboard.rs | 33 ++++------------------------ src/server/input_service.rs | 7 +++--- 3 files changed, 9 insertions(+), 33 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index ed2706382..7e3d0b0a4 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -201,6 +201,8 @@ message KeyEvent { bool press = 2; oneof union { ControlKey control_key = 3; + // high word, sym key code. win: virtual-key code, linux: keysym ?, macos: + // low word, position key code. win: scancode, linux: key code, macos: key code uint32 chr = 4; uint32 unicode = 5; string seq = 6; diff --git a/src/keyboard.rs b/src/keyboard.rs index 9ca5a16f0..02f34132f 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -785,34 +785,9 @@ fn is_hot_key_modifiers_down() -> bool { return false; } -pub fn translate_virtual_keycode(event: &Event, mut key_event: KeyEvent) -> Option { - match event.event_type { - EventType::KeyPress(..) => { - key_event.down = true; - } - EventType::KeyRelease(..) => { - key_event.down = false; - } - _ => return None, - }; - - let mut peer = get_peer_platform().to_lowercase(); - peer.retain(|c| !c.is_whitespace()); - - // #[cfg(target_os = "windows")] - // let keycode = match peer.as_str() { - // "windows" => event.code, - // "macos" => { - // if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { - // rdev::win_scancode_to_macos_iso_code(event.scan_code)? - // } else { - // rdev::win_scancode_to_macos_code(event.scan_code)? - // } - // } - // _ => rdev::win_scancode_to_linux_code(event.scan_code)?, - // }; - - key_event.set_chr(event.code as _); +pub fn translate_vk_scan_code(event: &Event, mut key_event: KeyEvent) -> Option { + let mut key_event = map_keyboard_mode(event, key_event)?; + key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.code as u32) << 16)); Some(key_event) } @@ -853,7 +828,7 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { - #[cfg(target_os = "windows")] - allow_err!(rdev::simulate_unicode(_unicode as _)); - } Some(key_event::Union::Seq(seq)) => { ENIGO.lock().unwrap().key_sequence(&seq); } @@ -1101,6 +1097,9 @@ fn translate_keyboard_mode(evt: &KeyEvent) { #[cfg(target_os = "windows")] translate_process_virtual_keycode(evt.chr(), evt.down) } + Some(key_event::Union::Unicode(..)) => { + // Do not handle unicode for now. + } _ => { log::debug!("Unreachable. Unexpected key event {:?}", &evt); } From 50ce57024c74ed9faab2099ed5c544be4e51e3a4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 13 Feb 2023 16:26:14 +0800 Subject: [PATCH 060/202] macos, win, translate mode, Signed-off-by: fufesou --- src/keyboard.rs | 1 + src/server/input_service.rs | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 02f34132f..8aa5f72d2 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -787,6 +787,7 @@ fn is_hot_key_modifiers_down() -> bool { pub fn translate_vk_scan_code(event: &Event, mut key_event: KeyEvent) -> Option { let mut key_event = map_keyboard_mode(event, key_event)?; + #[cfg(target_os = "windows")] key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.code as u32) << 16)); Some(key_event) } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 0f40cb7d6..18ff433a3 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1082,9 +1082,14 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } #[cfg(target_os = "windows")] -fn translate_process_virtual_keycode(vk: u32, down: bool) { +fn translate_process_code(code: u32, down: bool) { crate::platform::windows::try_change_desktop(); - sim_rdev_rawkey_virtual(vk, down); + let vk_code = + + match code >> 16 { + 0 => sim_rdev_rawkey_position(code, down), + vk_code => sim_rdev_rawkey_virtual(vk_code, down), + }; } fn translate_keyboard_mode(evt: &KeyEvent) { @@ -1095,7 +1100,9 @@ fn translate_keyboard_mode(evt: &KeyEvent) { Some(key_event::Union::Chr(..)) => { #[cfg(target_os = "windows")] - translate_process_virtual_keycode(evt.chr(), evt.down) + translate_process_code(evt.chr(), evt.down); + #[cfg(not(target_os = "windows"))] + sim_rdev_rawkey_position(code, down); } Some(key_event::Union::Unicode(..)) => { // Do not handle unicode for now. From 7dfcc401e5d59b53e6243211639ef990cc4a2384 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 15:42:02 +0800 Subject: [PATCH 061/202] translate mode, mac --> win, init debug Signed-off-by: fufesou --- Cargo.lock | 3 +- .../lib/desktop/widgets/remote_menubar.dart | 4 +- src/flutter_ffi.rs | 1 - src/keyboard.rs | 95 ++++++++++++++----- src/server/input_service.rs | 6 +- 5 files changed, 77 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0f66e287..2fcdef290 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4554,12 +4554,13 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#cedc4e62744566775026af4b434ef799804c1130" +source = "git+https://github.com/fufesou/rdev#593f0ba37139ed6f4f88a4120e972612ec4b1c6f" dependencies = [ "cocoa", "core-foundation 0.9.3", "core-foundation-sys 0.8.3", "core-graphics 0.22.3", + "dispatch", "enum-map", "epoll", "inotify", diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6bb49000b..9f8265fec 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1510,8 +1510,8 @@ class _RemoteMenubarState extends State { if (bind.sessionIsKeyboardModeSupported( id: widget.id, mode: mode.key)) { if (mode.key == 'translate') { - if (!Platform.isWindows || - widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) { + if (Platform.isLinux || + widget.ffi.ffiModel.pi.platform == kPeerPlatformLinux) { continue; } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b4e79b361..0e307abe3 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -20,7 +20,6 @@ use std::{ os::raw::c_char, str::FromStr, }; -use crate::ui_session_interface::InvokeUiSession; // use crate::hbbs_http::account::AuthResult; diff --git a/src/keyboard.rs b/src/keyboard.rs index 8aa5f72d2..7e4ba2b39 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -18,6 +18,13 @@ use std::{ #[cfg(windows)] static mut IS_ALT_GR: bool = false; +#[allow(dead_code)] +const OS_LOWER_WINDOWS: &str = "windows"; +#[allow(dead_code)] +const OS_LOWER_LINUX: &str = "linux"; +#[allow(dead_code)] +const OS_LOWER_MACOS: &str = "macos"; + #[cfg(any(target_os = "windows", target_os = "macos"))] static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); @@ -202,6 +209,9 @@ pub fn update_grab_get_key_name() { #[cfg(target_os = "windows")] static mut IS_0X021D_DOWN: bool = false; +#[cfg(target_os = "macos")] +static mut IS_LEFT_OPTION_DOWN: bool = false; + pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { @@ -213,6 +223,7 @@ pub fn start_grab_loop() { let mut _keyboard_mode = KeyboardMode::Map; let _scan_code = event.scan_code; + let _code = event.code; let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { _keyboard_mode = client::process_event(&event, None); if is_press { @@ -246,6 +257,13 @@ pub fn start_grab_loop() { } } + #[cfg(target_os = "macos")] + unsafe { + if _code as u32 == rdev::kVK_Option { + IS_LEFT_OPTION_DOWN = is_press; + } + } + return res; }; let func = move |event: Event| match event.event_type { @@ -253,11 +271,13 @@ pub fn start_grab_loop() { EventType::KeyRelease(key) => try_handle_keyboard(event, key, false), _ => Some(event), }; + #[cfg(target_os = "macos")] + rdev::set_is_main_thread(false); + #[cfg(target_os = "windows")] + rdev::set_event_popup(false); if let Err(error) = rdev::grab(func) { log::error!("rdev Error: {:?}", error) } - #[cfg(target_os = "windows")] - rdev::set_event_popup(false); }); #[cfg(target_os = "linux")] @@ -395,13 +415,16 @@ pub fn event_to_key_events( _ => {} } + let mut peer = get_peer_platform().to_lowercase(); + peer.retain(|c| !c.is_whitespace()); + key_event.mode = keyboard_mode.into(); let mut key_events = match keyboard_mode { - KeyboardMode::Map => match map_keyboard_mode(event, key_event) { + KeyboardMode::Map => match map_keyboard_mode(peer.as_str(), event, key_event) { Some(event) => [event].to_vec(), None => Vec::new(), }, - KeyboardMode::Translate => translate_keyboard_mode(event, key_event), + KeyboardMode::Translate => translate_keyboard_mode(peer.as_str(), event, key_event), _ => { #[cfg(not(any(target_os = "android", target_os = "ios")))] { @@ -424,7 +447,6 @@ pub fn event_to_key_events( } } } - key_events } @@ -698,7 +720,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec Option { +pub fn map_keyboard_mode(peer: &str, event: &Event, mut key_event: KeyEvent) -> Option { match event.event_type { EventType::KeyPress(..) => { key_event.down = true; @@ -709,12 +731,9 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option return None, }; - let mut peer = get_peer_platform().to_lowercase(); - peer.retain(|c| !c.is_whitespace()); - #[cfg(target_os = "windows")] - let keycode = match peer.as_str() { - "windows" => { + let keycode = match peer { + OS_LOWER_WINDOWS => { // https://github.com/rustdesk/rustdesk/issues/1371 // Filter scancodes that are greater than 255 and the hight word is not 0xE0. if event.scan_code > 255 && (event.scan_code >> 8) != 0xE0 { @@ -722,7 +741,7 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { + OS_LOWER_MACOS => { if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { rdev::win_scancode_to_macos_iso_code(event.scan_code)? } else { @@ -732,15 +751,15 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option rdev::win_scancode_to_linux_code(event.scan_code)?, }; #[cfg(target_os = "macos")] - let keycode = match peer.as_str() { - "windows" => rdev::macos_code_to_win_scancode(event.code as _)?, - "macos" => event.code as _, + let keycode = match peer { + OS_LOWER_WINDOWS => rdev::macos_code_to_win_scancode(event.code as _)?, + OS_LOWER_MACOS => event.code as _, _ => rdev::macos_code_to_linux_code(event.code as _)?, }; #[cfg(target_os = "linux")] - let keycode = match peer.as_str() { - "windows" => rdev::linux_code_to_win_scancode(event.code as _)?, - "macos" => { + let keycode = match peer { + OS_LOWER_WINDOWS => rdev::linux_code_to_win_scancode(event.code as _)?, + OS_LOWER_MACOS => { if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { rdev::linux_code_to_macos_iso_code(event.code as _)? } else { @@ -759,10 +778,10 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option) { match &event.unicode { Some(unicode_info) => { - if let Some(name) = unicode_info.name { + if let Some(name) = &unicode_info.name { if name.len() > 0 { let mut evt = key_event.clone(); - evt.set_seq(name); + evt.set_seq(name.to_string()); events.push(evt); } } @@ -785,21 +804,42 @@ fn is_hot_key_modifiers_down() -> bool { return false; } -pub fn translate_vk_scan_code(event: &Event, mut key_event: KeyEvent) -> Option { +#[inline] +#[cfg(target_os = "windows")] +pub fn translate_key_code(event: &Event, mut key_event: KeyEvent) -> Option { let mut key_event = map_keyboard_mode(event, key_event)?; - #[cfg(target_os = "windows")] key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.code as u32) << 16)); Some(key_event) } -pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { +#[inline] +#[cfg(not(target_os = "windows"))] +pub fn translate_key_code(peer: &str, event: &Event, key_event: KeyEvent) -> Option { + map_keyboard_mode(peer, event, key_event) +} + +pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -> Vec { let mut events: Vec = Vec::new(); if let Some(unicode_info) = &event.unicode { if unicode_info.is_dead { + #[cfg(target_os = "macos")] + if peer != OS_LOWER_MACOS && unsafe { IS_LEFT_OPTION_DOWN } { + // try clear dead key state + // rdev::clear_dead_key_state(); + } else { + return events; + } + #[cfg(not(target_os = "macos"))] return events; } } + #[cfg(target_os = "macos")] + // ignore right option key + if event.code as u32 == rdev::kVK_RightOption { + return events; + } + #[cfg(target_os = "windows")] unsafe { if event.scan_code == 0x021D { @@ -825,11 +865,16 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { - ENIGO.lock().unwrap().key_sequence(&seq); + ENIGO.lock().unwrap().key_sequence(seq); } Some(key_event::Union::Chr(..)) => { #[cfg(target_os = "windows")] translate_process_code(evt.chr(), evt.down); #[cfg(not(target_os = "windows"))] - sim_rdev_rawkey_position(code, down); + sim_rdev_rawkey_position(evt.chr(), evt.down); } Some(key_event::Union::Unicode(..)) => { // Do not handle unicode for now. From b2d13647be0a84be2047194f7786346bbbd049f2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 15:58:36 +0800 Subject: [PATCH 062/202] translate mode, mac --> win, debug 2 Signed-off-by: fufesou --- libs/enigo/src/win/win_impl.rs | 42 ++++++++++++++++------------------ src/keyboard.rs | 4 ++-- src/server/input_service.rs | 2 -- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index 2e1108b9e..115cb9789 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -39,7 +39,7 @@ fn mouse_event(flags: u32, data: u32, dx: i32, dy: i32) -> DWORD { unsafe { SendInput(1, &mut input as LPINPUT, size_of::() as c_int) } } -fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { +fn keybd_event(mut flags: u32, vk: u16, scan: u16) -> DWORD { let mut scan = scan; unsafe { // https://github.com/rustdesk/rustdesk/issues/366 @@ -52,35 +52,33 @@ fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { scan = MapVirtualKeyExW(vk as _, 0, LAYOUT) as _; } } - let mut input: INPUT = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; - input.type_ = INPUT_KEYBOARD; + + if flags & KEYEVENTF_UNICODE == 0 { + if scan >> 8 == 0xE0 || scan >> 8 == 0xE1 { + flags |= winapi::um::winuser::KEYEVENTF_EXTENDEDKEY; + } + } + let mut union: INPUT_u = unsafe { std::mem::zeroed() }; unsafe { - let dst_ptr = (&mut input.u as *mut _) as *mut u8; - let flags = match vk as _ { - winapi::um::winuser::VK_HOME | - winapi::um::winuser::VK_UP | - winapi::um::winuser::VK_PRIOR | - winapi::um::winuser::VK_LEFT | - winapi::um::winuser::VK_RIGHT | - winapi::um::winuser::VK_END | - winapi::um::winuser::VK_DOWN | - winapi::um::winuser::VK_NEXT | - winapi::um::winuser::VK_INSERT | - winapi::um::winuser::VK_DELETE => flags | winapi::um::winuser::KEYEVENTF_EXTENDEDKEY, - _ => flags, - }; - - let k = KEYBDINPUT { + *union.ki_mut() = KEYBDINPUT { wVk: vk, wScan: scan, dwFlags: flags, time: 0, dwExtraInfo: ENIGO_INPUT_EXTRA_VALUE, }; - let src_ptr = (&k as *const _) as *const u8; - std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, size_of::()); } - unsafe { SendInput(1, &mut input as LPINPUT, size_of::() as c_int) } + let mut inputs = [INPUT { + type_: INPUT_KEYBOARD, + u: union, + }; 1]; + unsafe { + SendInput( + inputs.len() as UINT, + inputs.as_mut_ptr(), + size_of::() as c_int, + ) + } } fn get_error() -> String { diff --git a/src/keyboard.rs b/src/keyboard.rs index 7e4ba2b39..4dcbe5c97 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -806,8 +806,8 @@ fn is_hot_key_modifiers_down() -> bool { #[inline] #[cfg(target_os = "windows")] -pub fn translate_key_code(event: &Event, mut key_event: KeyEvent) -> Option { - let mut key_event = map_keyboard_mode(event, key_event)?; +pub fn translate_key_code(peer: &str, event: &Event, key_event: KeyEvent) -> Option { + let mut key_event = map_keyboard_mode(peer, event, key_event)?; key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.code as u32) << 16)); Some(key_event) } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 59f503a14..67267bd94 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1084,8 +1084,6 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { #[cfg(target_os = "windows")] fn translate_process_code(code: u32, down: bool) { crate::platform::windows::try_change_desktop(); - let vk_code = - match code >> 16 { 0 => sim_rdev_rawkey_position(code, down), vk_code => sim_rdev_rawkey_virtual(vk_code, down), From a20f6b7d5e442f305d4058818d80243093c9a2eb Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 17:11:27 +0800 Subject: [PATCH 063/202] translate mode, fix win dead key Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2fcdef290..b308de149 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4554,7 +4554,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#593f0ba37139ed6f4f88a4120e972612ec4b1c6f" +source = "git+https://github.com/fufesou/rdev#5b9fb5e42117f44e0ce0fe7cf2bddf270c75f1dc" dependencies = [ "cocoa", "core-foundation 0.9.3", From e24f72040e5577d6ed44c73f4b45635b712a19f7 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 22:09:25 +0800 Subject: [PATCH 064/202] translate mode, trivial changes Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 9f8265fec..1a1a558fa 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1515,8 +1515,11 @@ class _RemoteMenubarState extends State { continue; } } - list.add(MenuEntryRadioOption( - text: translate(mode.menu), value: mode.key)); + var text = translate(mode.menu); + if (mode.key == 'translate') { + text = '$text beta legacy 2'; + } + list.add(MenuEntryRadioOption(text: text, value: mode.key)); } } return list; From 16dd1f3c797c7a6015d9cfadef4ea33e1f8d6d67 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 22:20:12 +0800 Subject: [PATCH 065/202] translate mode, trivial changes Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 1a1a558fa..66a13f606 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1517,7 +1517,7 @@ class _RemoteMenubarState extends State { } var text = translate(mode.menu); if (mode.key == 'translate') { - text = '$text beta legacy 2'; + text = '$text beta'; } list.add(MenuEntryRadioOption(text: text, value: mode.key)); } From 20be9e10b11e02b6e463ce3390abe0ab2670cfc7 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 14 Feb 2023 11:50:04 +0800 Subject: [PATCH 066/202] opt: scrollable on menubar, avoid overflow --- flutter/lib/desktop/widgets/remote_menubar.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 66a13f606..c68b394e4 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -439,9 +439,12 @@ class _RemoteMenubarState extends State { color: Colors.white, border: Border.all(color: MyTheme.border), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: menubarItems, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + children: menubarItems, + ), )), _buildDraggableShowHide(context), ])); From 8df357c9411faa4a23908a3aeb6fd72414634f60 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 15 Feb 2023 15:03:19 +0800 Subject: [PATCH 067/202] refactor: use listview for file lists --- flutter/lib/consts.dart | 12 + .../lib/desktop/pages/file_manager_page.dart | 298 ++++++++++-------- flutter/lib/models/file_model.dart | 4 + 3 files changed, 186 insertions(+), 128 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 26e25a209..2b4bc7f32 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -50,6 +50,18 @@ const int kMobileMaxDisplayHeight = 1280; const int kDesktopMaxDisplayWidth = 1920; const int kDesktopMaxDisplayHeight = 1080; +const double kDesktopFileTransferNameColWidth = 200; +const double kDesktopFileTransferModifiedColWidth = 120; +const double kDesktopFileTransferRowHeight = 25.0; +const double kDesktopFileTransferHeaderHeight = 25.0; + +// https://en.wikipedia.org/wiki/Non-breaking_space +const int $nbsp = 0x00A0; + +extension StringExtension on String { + String get nonBreaking => replaceAll(' ', String.fromCharCode($nbsp)); +} + const Size kConnectionManagerWindowSize = Size(300, 400); // Tabbar transition duration, now we remove the duration const Duration kTabTransitionDuration = Duration.zero; diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 27bb0377d..fef0dd3d3 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -236,10 +236,7 @@ class _FileManagerPageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: SingleChildScrollView( - controller: scrollController, - child: _buildDataTable(context, isLocal, scrollController), - ), + child: _buildFileList(context, isLocal, scrollController), ) ], )), @@ -248,25 +245,11 @@ class _FileManagerPageState extends State ); } - Widget _buildDataTable( + Widget _buildFileList( BuildContext context, bool isLocal, ScrollController scrollController) { - const rowHeight = 25.0; final fd = model.getCurrentDir(isLocal); final entries = fd.entries; - final sortIndex = (SortBy style) { - switch (style) { - case SortBy.name: - return 0; - case SortBy.type: - return 0; - case SortBy.modified: - return 1; - case SortBy.size: - return 2; - } - }(model.getSortStyle(isLocal)); - final sortAscending = - isLocal ? model.localSortAscending : model.remoteSortAscending; + final selectedEntries = getSelectedItems(isLocal); return MouseRegion( onEnter: (evt) { @@ -287,7 +270,6 @@ class _FileManagerPageState extends State onNext: (buffer) { debugPrint("searching next for $buffer"); assert(buffer.length == 1); - final selectedEntries = getSelectedItems(isLocal); assert(selectedEntries.length <= 1); var skipCount = 0; if (selectedEntries.items.isNotEmpty) { @@ -312,7 +294,8 @@ class _FileManagerPageState extends State return; } _jumpToEntry( - isLocal, searchResult.first, scrollController, rowHeight, buffer); + isLocal, searchResult.first, scrollController, + kDesktopFileTransferRowHeight, buffer); }, onSearch: (buffer) { debugPrint("searching for $buffer"); @@ -327,7 +310,8 @@ class _FileManagerPageState extends State return; } _jumpToEntry( - isLocal, searchResult.first, scrollController, rowHeight, buffer); + isLocal, searchResult.first, scrollController, + kDesktopFileTransferRowHeight, buffer); }, child: ObxValue( (searchText) { @@ -336,118 +320,120 @@ class _FileManagerPageState extends State return element.name.contains(searchText.value); }).toList(growable: false) : entries; - return DataTable( - key: ValueKey(isLocal ? 0 : 1), - showCheckboxColumn: false, - dataRowHeight: rowHeight, - headingRowHeight: 30, - horizontalMargin: 8, - columnSpacing: 8, - showBottomBorder: true, - sortColumnIndex: sortIndex, - sortAscending: sortAscending, - columns: [ - DataColumn( - label: Text( - translate("Name"), - ).marginSymmetric(horizontal: 4), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.name, - isLocal: isLocal, ascending: ascending); - }), - DataColumn( - label: Text( - translate("Modified"), - ), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.modified, - isLocal: isLocal, ascending: ascending); - }), - DataColumn( - label: Text(translate("Size")), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.size, - isLocal: isLocal, ascending: ascending); - }), - ], - rows: filteredEntries.map((entry) { + final rows = filteredEntries.map((entry) { final sizeStr = entry.isFile ? readableFileSize(entry.size.toDouble()) : ""; final lastModifiedStr = entry.isDrive ? " " : "${entry.lastModified().toString().replaceAll(".000", "")} "; - return DataRow( - key: ValueKey(entry.name), - onSelectChanged: (s) { - _onSelectedChanged(getSelectedItems(isLocal), - filteredEntries, entry, isLocal); - }, - selected: getSelectedItems(isLocal).contains(entry), - cells: [ - DataCell( - Container( - width: 200, - child: Tooltip( - waitDuration: Duration(milliseconds: 500), - message: entry.name, - child: Row(children: [ - entry.isDrive - ? Image( - image: iconHardDrive, - fit: BoxFit.scaleDown, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7)) - .paddingAll(4) - : Icon( - entry.isFile - ? Icons.feed_outlined - : Icons.folder, - size: 20, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7), - ).marginSymmetric(horizontal: 2), - Expanded( - child: Text(entry.name, - overflow: TextOverflow.ellipsis)) - ]), + final isSelected = selectedEntries.contains(entry); + return SizedBox( + key: ValueKey(entry.name), + height: kDesktopFileTransferRowHeight, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Divider( + height: 1, + ), + Expanded( + child: Ink( + decoration: isSelected + ? BoxDecoration(color: Theme.of(context).hoverColor) + : null, + child: InkWell( + child: Row(children: [ + GestureDetector( + child: Container( + width: kDesktopFileTransferNameColWidth, + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: entry.name, + child: Row(children: [ + entry.isDrive + ? Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)) + .paddingAll(4) + : Icon( + entry.isFile + ? Icons.feed_outlined + : Icons.folder, + size: 20, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7), + ).marginSymmetric(horizontal: 2), + Expanded( + child: Text(entry.name.nonBreaking, + overflow: TextOverflow.ellipsis)) + ]), + )), + onTap: () { + final items = getSelectedItems(isLocal); + // handle double click + if (_checkDoubleClick(entry)) { + openDirectory(entry.path, isLocal: isLocal); + items.clear(); + return; + } + _onSelectedChanged( + items, filteredEntries, entry, isLocal); + }, + ), + GestureDetector( + child: SizedBox( + width: kDesktopFileTransferModifiedColWidth, + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )), )), - onTap: () { - final items = getSelectedItems(isLocal); - - // handle double click - if (_checkDoubleClick(entry)) { - openDirectory(entry.path, isLocal: isLocal); - items.clear(); - return; - } - _onSelectedChanged( - items, filteredEntries, entry, isLocal); - }, + GestureDetector( + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: sizeStr, + child: Text( + sizeStr, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10, + color: MyTheme.darkGray), + ))), + ]), + ), ), - DataCell(FittedBox( - child: Tooltip( - waitDuration: Duration(milliseconds: 500), - message: lastModifiedStr, - child: Text( - lastModifiedStr, - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )))), - DataCell(Tooltip( - waitDuration: Duration(milliseconds: 500), - message: sizeStr, - child: Text( - sizeStr, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 10, color: MyTheme.darkGray), - ))), - ]); - }).toList(growable: false), + ), + ], + ), + ); + }).toList(growable: false); + + return Column( + children: [ + // Header + _buildFileBrowserHeader(context, isLocal), + // Body + Expanded( + child: ListView.builder( + controller: scrollController, + itemExtent: kDesktopFileTransferRowHeight, + itemBuilder: (context, index) { + return rows[index]; + }, + itemCount: rows.length, + ), + ), + ], ); }, isLocal ? _searchTextLocal : _searchTextRemote, @@ -1133,4 +1119,60 @@ class _FileManagerPageState extends State } }); } + + Widget headerItemFunc( + double? width, SortBy sortBy, String name, bool isLocal) { + final headerTextStyle = + Theme.of(context).dataTableTheme.headingTextStyle ?? TextStyle(); + return ObxValue>( + (ascending) => InkWell( + onTap: () { + if (ascending.value == null) { + ascending.value = true; + } else { + ascending.value = !ascending.value!; + } + model.changeSortStyle(sortBy, + isLocal: isLocal, ascending: ascending.value!); + }, + child: SizedBox( + width: width, + height: kDesktopFileTransferHeaderHeight, + child: Row( + children: [ + Text( + name, + style: headerTextStyle, + ).marginSymmetric( + horizontal: sortBy == SortBy.name ? 4 : 0.0), + ascending.value != null + ? Icon(ascending.value! + ? Icons.arrow_upward + : Icons.arrow_downward) + : const Offstage() + ], + ), + ), + ), () { + if (model.getSortStyle(isLocal) == sortBy) { + return model.getSortAscending(isLocal).obs; + } else { + return Rx(null); + } + }()); + } + + Widget _buildFileBrowserHeader(BuildContext context, bool isLocal) { + return Row( + children: [ + headerItemFunc(kDesktopFileTransferNameColWidth, SortBy.name, + translate("Name"), isLocal), + headerItemFunc(kDesktopFileTransferModifiedColWidth, SortBy.modified, + translate("Modified"), isLocal), + Expanded( + child: + headerItemFunc(null, SortBy.size, translate("Size"), isLocal)) + ], + ); + } } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 18d42d143..5817e54fe 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -75,6 +75,10 @@ class FileModel extends ChangeNotifier { return isLocal ? _localSortStyle : _remoteSortStyle; } + bool getSortAscending(bool isLocal) { + return isLocal ? _localSortAscending : _remoteSortAscending; + } + FileDirectory _currentLocalDir = FileDirectory(); FileDirectory get currentLocalDir => _currentLocalDir; From 66378f63d9bb329bfa659380fc6b6de17f17b37d Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 15 Feb 2023 15:25:28 +0800 Subject: [PATCH 068/202] fix macos command-tab Signed-off-by: fufesou --- src/server/input_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 67267bd94..917a815bb 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -719,7 +719,7 @@ fn reset_input() { let _lock = VIRTUAL_INPUT_MTX.lock(); VIRTUAL_INPUT = VirtualInput::new( CGEventSourceStateID::Private, - CGEventTapLocation::AnnotatedSession, + CGEventTapLocation::Session, ) .ok(); } From 2047fd822b97659f291d02c6f573503a03eb2b8e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 15 Feb 2023 16:44:40 +0800 Subject: [PATCH 069/202] opt: early unlock frame --- flutter/lib/models/model.dart | 10 ++++++---- flutter/lib/utils/image.dart | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8cf90eba9..a1d9ff0df 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -438,15 +438,17 @@ class ImageModel with ChangeNotifier { } final pid = parent.target?.id; - ui.decodeImageFromPixels( + img.decodeImageFromPixels( rgba, parent.target?.ffiModel.display.width ?? 0, parent.target?.ffiModel.display.height ?? 0, - isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { + isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, + onPixelsCopied: () { + // Unlock the rgba memory from rust codes. + platformFFI.nextRgba(id); + }).then((image) { if (parent.target?.id != pid) return; try { - // Unlock the rgba memory from rust codes. - platformFFI.nextRgba(id); // my throw exception, because the listener maybe already dispose update(image); } catch (e) { diff --git a/flutter/lib/utils/image.dart b/flutter/lib/utils/image.dart index 7a6bcbc15..a153dbc63 100644 --- a/flutter/lib/utils/image.dart +++ b/flutter/lib/utils/image.dart @@ -11,6 +11,7 @@ Future decodeImageFromPixels( int? rowBytes, int? targetWidth, int? targetHeight, + VoidCallback? onPixelsCopied, bool allowUpscaling = true, }) async { if (targetWidth != null) { @@ -22,6 +23,7 @@ Future decodeImageFromPixels( final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(pixels); + onPixelsCopied?.call(); final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw( buffer, width: width, From c5d39b0c105cf95f987be60bd6d573a7ba89aa03 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 11:40:17 +0100 Subject: [PATCH 070/202] reworked --- flutter/assets/actions.svg | 3 +- flutter/assets/chat.svg | 2 +- flutter/assets/close.svg | 2 +- flutter/assets/display.svg | 2 +- flutter/assets/fullscreen.svg | 2 +- flutter/assets/fullscreen_exit.svg | 2 +- flutter/assets/keyboard.svg | 2 +- flutter/assets/pinned.svg | 2 +- flutter/assets/rec.svg | 2 +- flutter/assets/unpinned.svg | 2 +- flutter/lib/desktop/pages/remote_page.dart | 2 +- .../lib/desktop/pages/remote_tab_page.dart | 9 ++- .../widgets/material_mod_popup_menu.dart | 9 +-- flutter/lib/desktop/widgets/menu_button.dart | 63 +++++++++++++++++ .../lib/desktop/widgets/remote_menubar.dart | 67 +++++++++++-------- 15 files changed, 125 insertions(+), 46 deletions(-) create mode 100644 flutter/lib/desktop/widgets/menu_button.dart diff --git a/flutter/assets/actions.svg b/flutter/assets/actions.svg index feaf416cd..5403853db 100644 --- a/flutter/assets/actions.svg +++ b/flutter/assets/actions.svg @@ -1,3 +1,2 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg index 830ef0d33..7088107b0 100644 --- a/flutter/assets/chat.svg +++ b/flutter/assets/chat.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/close.svg b/flutter/assets/close.svg index 1e9a30711..7488acc9f 100644 --- a/flutter/assets/close.svg +++ b/flutter/assets/close.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/display.svg b/flutter/assets/display.svg index 8a87116ff..b5a88106e 100644 --- a/flutter/assets/display.svg +++ b/flutter/assets/display.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/fullscreen.svg b/flutter/assets/fullscreen.svg index 73d79cf0e..cd01f93f9 100644 --- a/flutter/assets/fullscreen.svg +++ b/flutter/assets/fullscreen.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/fullscreen_exit.svg b/flutter/assets/fullscreen_exit.svg index f2b3ae27b..8d4414897 100644 --- a/flutter/assets/fullscreen_exit.svg +++ b/flutter/assets/fullscreen_exit.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg index 569c68727..d5481d7a1 100644 --- a/flutter/assets/keyboard.svg +++ b/flutter/assets/keyboard.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/pinned.svg b/flutter/assets/pinned.svg index 2563015f7..dd718b96a 100644 --- a/flutter/assets/pinned.svg +++ b/flutter/assets/pinned.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/rec.svg b/flutter/assets/rec.svg index 14546b971..33a57e9d0 100644 --- a/flutter/assets/rec.svg +++ b/flutter/assets/rec.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/unpinned.svg b/flutter/assets/unpinned.svg index ba4ab5328..9e9e3de8b 100644 --- a/flutter/assets/unpinned.svg +++ b/flutter/assets/unpinned.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 211d36c39..dac62032f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -201,7 +201,7 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay /// see override build() in [BlockableOverlay] diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 9b00b481f..610a7d1a5 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -22,7 +22,10 @@ import 'package:bot_toast/bot_toast.dart'; import '../../models/platform_model.dart'; class _MenuTheme { - static const Color commonColor = MyTheme.accent; + static const Color blueColor = MyTheme.button; + static const Color hoverBlueColor = MyTheme.accent; + static const Color redColor = Colors.redAccent; + static const Color hoverRedColor = Colors.red; // kMinInteractiveDimension static const double height = 20.0; static const double dividerHeight = 12.0; @@ -134,7 +137,7 @@ class _ConnectionTabPageState extends State { width: stateGlobal.windowBorderWidth.value), ), child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, @@ -280,7 +283,7 @@ class _ConnectionTabPageState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenuTheme.commonColor, + commonColor: _MenuTheme.blueColor, height: _MenuTheme.height, dividerHeight: _MenuTheme.dividerHeight, ))) diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 666c9a6e2..05c3059d4 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/widgets/menu_button.dart'; // Examples can assume: // enum Commands { heroAndScholar, hurricaneCame } @@ -1391,22 +1393,21 @@ class PopupMenuButtonState extends State> { onTap: widget.enabled ? showButtonMenu : null, onHover: widget.onHover, canRequestFocus: _canRequestFocus, - radius: widget.splashRadius, enableFeedback: enableFeedback, child: widget.child, ), ); } - return IconButton( + return MenuButton( icon: widget.icon ?? Icon(Icons.adaptive.more), - padding: widget.padding, - splashRadius: widget.splashRadius, iconSize: widget.iconSize ?? iconTheme.size ?? _kDefaultIconSize, tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: widget.enabled ? showButtonMenu : null, enableFeedback: enableFeedback, + color: MyTheme.button, + hoverColor: MyTheme.accent, ); } } diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart new file mode 100644 index 000000000..ce63dcab1 --- /dev/null +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +class MenuButton extends StatefulWidget { + final GestureTapCallback? onPressed; + final Color color; + final Color hoverColor; + final Color? splashColor; + final Widget icon; + final double iconSize; + final String tooltip; + final EdgeInsetsGeometry padding; + final bool enableFeedback; + const MenuButton({ + super.key, + required this.onPressed, + required this.color, + required this.hoverColor, + required this.icon, + required this.iconSize, + required this.tooltip, + this.splashColor, + this.padding = const EdgeInsets.all(5), + this.enableFeedback = true, + }); + + @override + State createState() => _MenuButtonState(); +} + +class _MenuButtonState extends State { + bool _isHover = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: widget.padding, + child: Tooltip( + message: widget.tooltip, + child: Material( + type: MaterialType.transparency, + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: _isHover ? widget.hoverColor : widget.color, + ), + child: InkWell( + onHover: (val) { + setState(() { + _isHover = val; + }); + }, + borderRadius: BorderRadius.circular(5), + splashColor: widget.splashColor, + enableFeedback: widget.enableFeedback, + onTap: widget.onPressed, + child: widget.icon, + ), + ), + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 77d687d93..ff586a1f1 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -5,6 +5,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/desktop/widgets/menu_button.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/consts.dart'; @@ -94,7 +95,10 @@ class MenubarState { } class _MenubarTheme { - static const Color commonColor = MyTheme.accent; + static const Color blueColor = MyTheme.button; + static const Color hoverBlueColor = MyTheme.accent; + static const Color redColor = Colors.redAccent; + static const Color hoverRedColor = Colors.red; // kMinInteractiveDimension static const double height = 20.0; static const double dividerHeight = 12.0; @@ -412,7 +416,7 @@ class _RemoteMenubarState extends State { if (widget.ffi.ffiModel.isPeerAndroid) { menubarItems.add(IconButton( tooltip: translate('Mobile Actions'), - color: _MenubarTheme.commonColor, + color: _MenubarTheme.blueColor, icon: const Icon(Icons.build), onPressed: () { widget.ffi.dialogManager @@ -433,7 +437,7 @@ class _RemoteMenubarState extends State { menubarItems.add(_buildClose(context, iconSize)); return PopupMenuTheme( data: const PopupMenuThemeData( - textStyle: TextStyle(color: _MenubarTheme.commonColor)), + textStyle: TextStyle(color: _MenubarTheme.blueColor)), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -457,8 +461,7 @@ class _RemoteMenubarState extends State { Widget _buildPinMenubar(BuildContext context, double iconSize) { return Obx( - () => IconButton( - padding: EdgeInsets.zero, + () => MenuButton( iconSize: iconSize, tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), onPressed: () { @@ -466,15 +469,16 @@ class _RemoteMenubarState extends State { }, icon: SvgPicture.asset( pin ? "assets/pinned.svg" : "assets/unpinned.svg", - color: pin ? _MenubarTheme.commonColor : Colors.grey[800], + color: Colors.white, ), + color: pin ? _MenubarTheme.blueColor : Colors.grey[800]!, + hoverColor: pin ? _MenubarTheme.hoverBlueColor : Colors.grey[850]!, ), ); } Widget _buildFullscreen(BuildContext context, double iconSize) { - return IconButton( - padding: EdgeInsets.zero, + return MenuButton( iconSize: iconSize, tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), onPressed: () { @@ -482,8 +486,10 @@ class _RemoteMenubarState extends State { }, icon: SvgPicture.asset( isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, ); } @@ -492,14 +498,13 @@ class _RemoteMenubarState extends State { return mod_menu.PopupMenuButton( iconSize: iconSize, tooltip: translate('Select Monitor'), - padding: EdgeInsets.zero, position: mod_menu.PopupMenuPosition.under, icon: Stack( alignment: Alignment.center, children: [ SvgPicture.asset( "assets/display.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), Padding( padding: const EdgeInsets.only(bottom: 3.9), @@ -520,7 +525,10 @@ class _RemoteMenubarState extends State { Stack( alignment: Alignment.center, children: [ - SvgPicture.asset("assets/display.svg"), + SvgPicture.asset( + "assets/display.svg", + color: Colors.white, + ), TextButton( child: Container( alignment: AlignmentDirectional.center, @@ -531,7 +539,7 @@ class _RemoteMenubarState extends State { child: Text( (i + 1).toString(), style: TextStyle( - color: Theme.of(context).scaffoldBackgroundColor, + color: Colors.white, ), ), ), @@ -573,7 +581,7 @@ class _RemoteMenubarState extends State { padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/actions.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), tooltip: translate('Control Actions'), position: mod_menu.PopupMenuPosition.under, @@ -581,7 +589,7 @@ class _RemoteMenubarState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenubarTheme.commonColor, + commonColor: _MenubarTheme.blueColor, height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) @@ -606,7 +614,7 @@ class _RemoteMenubarState extends State { padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/display.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), tooltip: translate('Display Settings'), position: mod_menu.PopupMenuPosition.under, @@ -616,7 +624,7 @@ class _RemoteMenubarState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenubarTheme.commonColor, + commonColor: _MenubarTheme.blueColor, height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) @@ -640,7 +648,7 @@ class _RemoteMenubarState extends State { padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/keyboard.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), tooltip: translate('Keyboard Settings'), position: mod_menu.PopupMenuPosition.under, @@ -648,7 +656,7 @@ class _RemoteMenubarState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenubarTheme.commonColor, + commonColor: _MenubarTheme.blueColor, height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) @@ -661,8 +669,7 @@ class _RemoteMenubarState extends State { return Consumer(builder: ((context, value, child) { if (value.permissions['recording'] != false) { return Consumer( - builder: (context, value, child) => IconButton( - padding: EdgeInsets.zero, + builder: (context, value, child) => MenuButton( iconSize: iconSize, tooltip: value.start ? translate('Stop session recording') @@ -670,8 +677,13 @@ class _RemoteMenubarState extends State { onPressed: () => value.toggle(), icon: SvgPicture.asset( "assets/rec.svg", - color: value.start ? Colors.red : _MenubarTheme.commonColor, + color: Colors.white, ), + color: + value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor, + hoverColor: value.start + ? _MenubarTheme.hoverRedColor + : _MenubarTheme.hoverBlueColor, ), ); } else { @@ -681,17 +693,18 @@ class _RemoteMenubarState extends State { } Widget _buildClose(BuildContext context, double iconSize) { - return IconButton( + return MenuButton( iconSize: iconSize, - padding: EdgeInsets.zero, tooltip: translate('Close'), onPressed: () { clientClose(widget.id, widget.ffi.dialogManager); }, icon: SvgPicture.asset( "assets/close.svg", - color: Colors.red, + color: Colors.white, ), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor, ); } @@ -704,7 +717,7 @@ class _RemoteMenubarState extends State { padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/chat.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), tooltip: translate('Chat'), position: mod_menu.PopupMenuPosition.under, @@ -712,7 +725,7 @@ class _RemoteMenubarState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenubarTheme.commonColor, + commonColor: _MenubarTheme.blueColor, height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) From 952596080279c8778cd3cd7edd8af6da80fa9089 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 13:19:15 +0100 Subject: [PATCH 071/202] added new call end/wait icons --- flutter/assets/call_end.svg | 2 + flutter/assets/call_wait.svg | 2 + flutter/lib/desktop/pages/remote_page.dart | 2 +- .../lib/desktop/pages/remote_tab_page.dart | 2 +- .../widgets/material_mod_popup_menu.dart | 1 - flutter/lib/desktop/widgets/menu_button.dart | 6 +- .../lib/desktop/widgets/remote_menubar.dart | 79 +++++++------------ 7 files changed, 38 insertions(+), 56 deletions(-) create mode 100644 flutter/assets/call_end.svg create mode 100644 flutter/assets/call_wait.svg diff --git a/flutter/assets/call_end.svg b/flutter/assets/call_end.svg new file mode 100644 index 000000000..39367c3c5 --- /dev/null +++ b/flutter/assets/call_end.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/call_wait.svg b/flutter/assets/call_wait.svg new file mode 100644 index 000000000..42a11fe56 --- /dev/null +++ b/flutter/assets/call_wait.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index dac62032f..211d36c39 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -201,7 +201,7 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).backgroundColor, /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay /// see override build() in [BlockableOverlay] diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 610a7d1a5..7bd2a4126 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -137,7 +137,7 @@ class _ConnectionTabPageState extends State { width: stateGlobal.windowBorderWidth.value), ), child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).backgroundColor, body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 05c3059d4..47de1be20 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -1401,7 +1401,6 @@ class PopupMenuButtonState extends State> { return MenuButton( icon: widget.icon ?? Icon(Icons.adaptive.more), - iconSize: widget.iconSize ?? iconTheme.size ?? _kDefaultIconSize, tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: widget.enabled ? showButtonMenu : null, diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index ce63dcab1..b2871e0cd 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -6,8 +6,7 @@ class MenuButton extends StatefulWidget { final Color hoverColor; final Color? splashColor; final Widget icon; - final double iconSize; - final String tooltip; + final String? tooltip; final EdgeInsetsGeometry padding; final bool enableFeedback; const MenuButton({ @@ -16,9 +15,8 @@ class MenuButton extends StatefulWidget { required this.color, required this.hoverColor, required this.icon, - required this.iconSize, - required this.tooltip, this.splashColor, + this.tooltip = "", this.padding = const EdgeInsets.all(5), this.enableFeedback = true, }); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index ff586a1f1..5029560b0 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -409,10 +409,9 @@ class _RemoteMenubarState extends State { Widget _buildMenubar(BuildContext context) { final List menubarItems = []; - final double iconSize = Theme.of(context).iconTheme.size ?? 30.0; if (!isWebDesktop) { - menubarItems.add(_buildPinMenubar(context, iconSize)); - menubarItems.add(_buildFullscreen(context, iconSize)); + menubarItems.add(_buildPinMenubar(context)); + menubarItems.add(_buildFullscreen(context)); if (widget.ffi.ffiModel.isPeerAndroid) { menubarItems.add(IconButton( tooltip: translate('Mobile Actions'), @@ -425,16 +424,16 @@ class _RemoteMenubarState extends State { )); } } - menubarItems.add(_buildMonitor(context, iconSize)); - menubarItems.add(_buildControl(context, iconSize)); - menubarItems.add(_buildDisplay(context, iconSize)); - menubarItems.add(_buildKeyboard(context, iconSize)); + menubarItems.add(_buildMonitor(context)); + menubarItems.add(_buildControl(context)); + menubarItems.add(_buildDisplay(context)); + menubarItems.add(_buildKeyboard(context)); if (!isWeb) { - menubarItems.add(_buildChat(context, iconSize)); - menubarItems.add(_buildVoiceCall(context, iconSize)); + menubarItems.add(_buildChat(context)); + menubarItems.add(_buildVoiceCall(context)); } - menubarItems.add(_buildRecording(context, iconSize)); - menubarItems.add(_buildClose(context, iconSize)); + menubarItems.add(_buildRecording(context)); + menubarItems.add(_buildClose(context)); return PopupMenuTheme( data: const PopupMenuThemeData( textStyle: TextStyle(color: _MenubarTheme.blueColor)), @@ -459,10 +458,9 @@ class _RemoteMenubarState extends State { ); } - Widget _buildPinMenubar(BuildContext context, double iconSize) { + Widget _buildPinMenubar(BuildContext context) { return Obx( () => MenuButton( - iconSize: iconSize, tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), onPressed: () { widget.state.switchPin(); @@ -477,9 +475,8 @@ class _RemoteMenubarState extends State { ); } - Widget _buildFullscreen(BuildContext context, double iconSize) { + Widget _buildFullscreen(BuildContext context) { return MenuButton( - iconSize: iconSize, tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), onPressed: () { _setFullscreen(!isFullscreen); @@ -493,10 +490,9 @@ class _RemoteMenubarState extends State { ); } - Widget _buildMonitor(BuildContext context, double iconSize) { + Widget _buildMonitor(BuildContext context) { final pi = widget.ffi.ffiModel.pi; return mod_menu.PopupMenuButton( - iconSize: iconSize, tooltip: translate('Select Monitor'), position: mod_menu.PopupMenuPosition.under, icon: Stack( @@ -575,9 +571,8 @@ class _RemoteMenubarState extends State { ); } - Widget _buildControl(BuildContext context, double iconSize) { + Widget _buildControl(BuildContext context) { return mod_menu.PopupMenuButton( - iconSize: iconSize, padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/actions.svg", @@ -598,7 +593,7 @@ class _RemoteMenubarState extends State { ); } - Widget _buildDisplay(BuildContext context, double iconSize) { + Widget _buildDisplay(BuildContext context) { return FutureBuilder(future: () async { widget.state.viewStyle.value = await bind.sessionGetViewStyle(id: widget.id) ?? ''; @@ -610,7 +605,6 @@ class _RemoteMenubarState extends State { return Obx(() { final remoteCount = RemoteCountState.find().value; return mod_menu.PopupMenuButton( - iconSize: iconSize, padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/display.svg", @@ -638,13 +632,12 @@ class _RemoteMenubarState extends State { }); } - Widget _buildKeyboard(BuildContext context, double iconSize) { + Widget _buildKeyboard(BuildContext context) { FfiModel ffiModel = Provider.of(context); if (ffiModel.permissions['keyboard'] == false) { return Offstage(); } return mod_menu.PopupMenuButton( - iconSize: iconSize, padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/keyboard.svg", @@ -665,12 +658,11 @@ class _RemoteMenubarState extends State { ); } - Widget _buildRecording(BuildContext context, double iconSize) { + Widget _buildRecording(BuildContext context) { return Consumer(builder: ((context, value, child) { if (value.permissions['recording'] != false) { return Consumer( builder: (context, value, child) => MenuButton( - iconSize: iconSize, tooltip: value.start ? translate('Stop session recording') : translate('Start session recording'), @@ -692,9 +684,8 @@ class _RemoteMenubarState extends State { })); } - Widget _buildClose(BuildContext context, double iconSize) { + Widget _buildClose(BuildContext context) { return MenuButton( - iconSize: iconSize, tooltip: translate('Close'), onPressed: () { clientClose(widget.id, widget.ffi.dialogManager); @@ -709,10 +700,9 @@ class _RemoteMenubarState extends State { } final _chatButtonKey = GlobalKey(); - Widget _buildChat(BuildContext context, double iconSize) { + Widget _buildChat(BuildContext context) { FfiModel ffiModel = Provider.of(context); return mod_menu.PopupMenuButton( - iconSize: iconSize, key: _chatButtonKey, padding: EdgeInsets.zero, icon: SvgPicture.asset( @@ -737,24 +727,15 @@ class _RemoteMenubarState extends State { Widget _getVoiceCallIcon() { switch (widget.ffi.chatModel.voiceCallStatus.value) { case VoiceCallStatus.waitingForResponse: - return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: SvgPicture.asset( - "assets/voice_call_waiting.svg", - color: Colors.red, - ), + return SvgPicture.asset( + "assets/call_wait.svg", + color: Colors.white, ); + case VoiceCallStatus.connected: - return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: Icon( - Icons.phone_disabled_rounded, - color: Colors.red, - ), + return SvgPicture.asset( + "assets/call_end.svg", + color: Colors.white, ); default: return const Offstage(); @@ -772,18 +753,18 @@ class _RemoteMenubarState extends State { } } - Widget _buildVoiceCall(BuildContext context, double iconSize) { + Widget _buildVoiceCall(BuildContext context) { return Obx( () { final tooltipText = _getVoiceCallTooltip(); return tooltipText == null ? const Offstage() - : IconButton( - iconSize: iconSize, - padding: EdgeInsets.zero, + : MenuButton( icon: _getVoiceCallIcon(), tooltip: translate(tooltipText), onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor, ); }, ); From 957bb65b9f624d6b00377787033e545cf6423562 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 13:27:21 +0100 Subject: [PATCH 072/202] adjusted spacing --- flutter/lib/desktop/widgets/menu_button.dart | 2 +- flutter/lib/desktop/widgets/remote_menubar.dart | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index b2871e0cd..904195f71 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -17,7 +17,7 @@ class MenuButton extends StatefulWidget { required this.icon, this.splashColor, this.tooltip = "", - this.padding = const EdgeInsets.all(5), + this.padding = const EdgeInsets.symmetric(horizontal: 2.5, vertical: 5), this.enableFeedback = true, }); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 5029560b0..afc5b2d9f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -449,7 +449,11 @@ class _RemoteMenubarState extends State { ), child: Row( mainAxisSize: MainAxisSize.min, - children: menubarItems, + children: [ + SizedBox(width: 2.5), + ...menubarItems, + SizedBox(width: 2.5) + ], ), ), _buildDraggableShowHide(context), From d5502f58ef5c1c95554ad7917f9aa1eeab21d004 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 15 Feb 2023 20:39:30 +0800 Subject: [PATCH 073/202] release session stream after close Signed-off-by: fufesou --- flutter/lib/models/model.dart | 3 +++ src/flutter_ffi.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a1d9ff0df..865a8bea6 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1368,6 +1368,9 @@ class FFI { // Preserved for the rgba data. await for (final message in stream) { if (message is EventToUI_Event) { + if (message.field0 == "close") { + break; + } try { Map event = json.decode(message.field0); await cb(event); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0e307abe3..3f9940854 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -132,6 +132,9 @@ pub fn session_login(id: String, password: String, remember: bool) { pub fn session_close(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { + if let Some(stream) = &*session.event_stream.read().unwrap() { + stream.add(EventToUI::Event("close".to_owned())); + } session.close(); } let _ = SESSIONS.write().unwrap().remove(&id); From eac6dae3a7aed17b81916b9369c2ed94914f054f Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 14:14:21 +0100 Subject: [PATCH 074/202] increased margin --- flutter/lib/desktop/widgets/menu_button.dart | 2 +- flutter/lib/desktop/widgets/remote_menubar.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index 904195f71..7c9fe67eb 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -17,7 +17,7 @@ class MenuButton extends StatefulWidget { required this.icon, this.splashColor, this.tooltip = "", - this.padding = const EdgeInsets.symmetric(horizontal: 2.5, vertical: 5), + this.padding = const EdgeInsets.symmetric(horizontal: 3, vertical: 6), this.enableFeedback = true, }); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 189f58f4b..933850c99 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -452,9 +452,9 @@ class _RemoteMenubarState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - SizedBox(width: 2.5), + SizedBox(width: 3), ...menubarItems, - SizedBox(width: 2.5) + SizedBox(width: 3) ], ), ), From d8fe75860465a09fc5b80143069a7cd719cafb2f Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 15 Feb 2023 21:27:50 +0800 Subject: [PATCH 075/202] set event stream to None in rust side Signed-off-by: fufesou --- flutter/lib/models/model.dart | 1 + src/flutter.rs | 8 ++++++++ src/flutter_ffi.rs | 9 +++------ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 865a8bea6..39b1cdd03 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1389,6 +1389,7 @@ class FFI { } } } + debugPrint('Exit session event loop'); }(); // every instance will bind a stream this.id = id; diff --git a/src/flutter.rs b/src/flutter.rs index a60e379f9..d0f397d3f 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -134,6 +134,14 @@ impl FlutterHandler { stream.add(EventToUI::Event(out)); } } + + pub fn close_event_stream(&mut self) { + let mut stream_lock = self.event_stream.write().unwrap(); + if let Some(stream) = &*stream_lock { + stream.add(EventToUI::Event("close".to_owned())); + } + *stream_lock = None; + } } impl InvokeUiSession for FlutterHandler { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 3f9940854..53ddb724a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -6,7 +6,7 @@ use crate::{ flutter::{session_add, session_start_}, ui_interface::{self, *}, }; -use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; +use flutter_rust_bridge::{StreamSink, SyncReturn}; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, @@ -131,13 +131,10 @@ pub fn session_login(id: String, password: String, remember: bool) { } pub fn session_close(id: String) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - if let Some(stream) = &*session.event_stream.read().unwrap() { - stream.add(EventToUI::Event("close".to_owned())); - } + if let Some(mut session) = SESSIONS.write().unwrap().remove(&id) { + session.close_event_stream(); session.close(); } - let _ = SESSIONS.write().unwrap().remove(&id); } pub fn session_refresh(id: String) { From 432f0b7e3e3924c8f90704f76f07ba4b38f7bd4a Mon Sep 17 00:00:00 2001 From: grummbeer Date: Wed, 15 Feb 2023 15:20:09 +0100 Subject: [PATCH 076/202] CustomDialog. Add left padding to actions --- flutter/lib/common.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ba7e3d762..bdef5f638 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -665,7 +665,7 @@ class CustomAlertDialog extends StatelessWidget { child: content), ), actions: actions, - actionsPadding: EdgeInsets.fromLTRB(0, 0, padding, padding), + actionsPadding: EdgeInsets.fromLTRB(padding, 0, padding, padding), ), ); } From 8f64940147214b266cdae0f82dcb16660bcc5f08 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 20:17:36 +0100 Subject: [PATCH 077/202] changed linux icon --- flutter/assets/linux.svg | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg index 74248b5f0..5427305ba 100644 --- a/flutter/assets/linux.svg +++ b/flutter/assets/linux.svg @@ -1,6 +1,2 @@ - - - - - - + + \ No newline at end of file From 97ad7a42bdeb7ba6460aa24e562ae4bbe2dbbd4d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 16 Feb 2023 10:58:27 +0800 Subject: [PATCH 078/202] fix: window manager called on Android & bugfix etc. --- flutter/lib/common.dart | 3 +++ flutter/lib/desktop/widgets/remote_menubar.dart | 2 +- src/ui/header.tis | 3 --- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ba7e3d762..8a33f214c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -336,6 +336,9 @@ closeConnection({String? id}) { } void window_on_top(int? id) { + if (!isDesktop) { + return; + } if (id == null) { // main window windowManager.restore(); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 933850c99..0fa12cd6f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -769,7 +769,7 @@ class _RemoteMenubarState extends State { : MenuButton( icon: _getVoiceCallIcon(), tooltip: translate(tooltipText), - onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), + onPressed: () => bind.sessionCloseVoiceCall(id: widget.id), color: _MenubarTheme.redColor, hoverColor: _MenubarTheme.hoverRedColor, ); diff --git a/src/ui/header.tis b/src/ui/header.tis index 009995f4f..1fb694397 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -434,9 +434,6 @@ function toggleMenuState() { var c = handler.get_option("codec-preference"); if (!c) c = "auto"; values.push(c); - var a = handler.get_audio_mode(); - if (!a) a = "guest-to-host"; - values.push(a); for (var el in $$(menu#display-options li)) { el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); } From ed441242bf290b4df7fba366ce29d692baa84994 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 16 Feb 2023 14:54:13 +0800 Subject: [PATCH 079/202] add reconnect button on Connection Error Signed-off-by: 21pages --- flutter/lib/common.dart | 9 ++++++++- flutter/lib/models/model.dart | 32 +++++++++++++++++--------------- src/lang/ca.rs | 3 ++- src/lang/cn.rs | 5 +++-- src/lang/cs.rs | 3 ++- src/lang/da.rs | 3 ++- src/lang/de.rs | 3 ++- src/lang/eo.rs | 3 ++- src/lang/es.rs | 3 ++- src/lang/fa.rs | 3 ++- src/lang/fr.rs | 3 ++- src/lang/gr.rs | 3 ++- src/lang/hu.rs | 3 ++- src/lang/id.rs | 3 ++- src/lang/it.rs | 3 ++- src/lang/ja.rs | 3 ++- src/lang/ko.rs | 3 ++- src/lang/kz.rs | 3 ++- src/lang/nl.rs | 6 ++++-- src/lang/pl.rs | 3 ++- src/lang/pt_PT.rs | 3 ++- src/lang/ptbr.rs | 3 ++- src/lang/ro.rs | 3 ++- src/lang/ru.rs | 3 ++- src/lang/sk.rs | 3 ++- src/lang/sl.rs | 3 ++- src/lang/sq.rs | 3 ++- src/lang/sr.rs | 3 ++- src/lang/sv.rs | 3 ++- src/lang/template.rs | 3 ++- src/lang/th.rs | 3 ++- src/lang/tr.rs | 3 ++- src/lang/tw.rs | 11 ++++++----- src/lang/ua.rs | 3 ++- src/lang/vn.rs | 3 ++- 35 files changed, 98 insertions(+), 55 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 9f375860d..c01fe8910 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -676,7 +676,7 @@ class CustomAlertDialog extends StatelessWidget { void msgBox(String id, String type, String title, String text, String link, OverlayDialogManager dialogManager, - {bool? hasCancel}) { + {bool? hasCancel, ReconnectHandle? reconnect}) { dialogManager.dismissAll(); List buttons = []; bool hasOk = false; @@ -716,6 +716,13 @@ void msgBox(String id, String type, String title, String text, String link, dialogManager.dismissAll(); })); } + if (reconnect != null && title == "Connection Error") { + buttons.insert( + 0, + dialogButton('Reconnect', isOutline: true, onPressed: () { + reconnect(dialogManager, id, false); + })); + } if (link.isNotEmpty) { buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink)); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 28d3ae622..458ca29f4 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -33,6 +33,7 @@ import 'input_model.dart'; import 'platform_model.dart'; typedef HandleMsgBox = Function(Map evt, String id); +typedef ReconnectHandle = Function(OverlayDialogManager, String, bool); final _waitForImage = {}; class FfiModel with ChangeNotifier { @@ -310,14 +311,12 @@ class FfiModel with ChangeNotifier { showMsgBox(String id, String type, String title, String text, String link, bool hasRetry, OverlayDialogManager dialogManager, {bool? hasCancel}) { - msgBox(id, type, title, text, link, dialogManager, hasCancel: hasCancel); + msgBox(id, type, title, text, link, dialogManager, + hasCancel: hasCancel, reconnect: reconnect); _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { - bind.sessionReconnect(id: id, forceRelay: false); - clearPermissions(); - dialogManager.showLoading(translate('Connecting...'), - onCancel: closeConnection); + reconnect(dialogManager, id, false); }); _reconnects *= 2; } else { @@ -325,6 +324,14 @@ class FfiModel with ChangeNotifier { } } + void reconnect( + OverlayDialogManager dialogManager, String id, bool forceRelay) { + bind.sessionReconnect(id: id, forceRelay: forceRelay); + clearPermissions(); + dialogManager.showLoading(translate('Connecting...'), + onCancel: closeConnection); + } + void showRelayHintDialog(String id, String type, String title, String text, OverlayDialogManager dialogManager) { dialogManager.show(tag: '$id-$type', (setState, close) { @@ -333,13 +340,6 @@ class FfiModel with ChangeNotifier { close(); } - reconnect(bool forceRelay) { - bind.sessionReconnect(id: id, forceRelay: forceRelay); - clearPermissions(); - dialogManager.showLoading(translate('Connecting...'), - onCancel: closeConnection); - } - final style = ElevatedButton.styleFrom(backgroundColor: Colors.green[700]); return CustomAlertDialog( @@ -348,14 +348,16 @@ class FfiModel with ChangeNotifier { "${translate(text)}\n\n${translate('relay_hint_tip')}"), actions: [ dialogButton('Close', onPressed: onClose, isOutline: true), - dialogButton('Retry', onPressed: () => reconnect(false)), + dialogButton('Retry', + onPressed: () => reconnect(dialogManager, id, false)), dialogButton('Connect via relay', - onPressed: () => reconnect(true), buttonStyle: style), + onPressed: () => reconnect(dialogManager, id, true), + buttonStyle: style), dialogButton('Always connect via relay', onPressed: () { const option = 'force-always-relay'; bind.sessionPeerOption( id: id, name: option, value: bool2option(option, true)); - reconnect(true); + reconnect(dialogManager, id, true); }, buttonStyle: style), ], onCancel: onClose, diff --git a/src/lang/ca.rs b/src/lang/ca.rs index d483a185d..3220c824a 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 7dea516ba..d0fdcb3fd 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -422,7 +422,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ask the remote user for authentication", "请求远端用户授权"), ("Choose this if the remote account is administrator", "当对面电脑是管理员账号时选择该选项"), ("Transmit the username and password of administrator", "发送管理员账号的用户名密码"), - ("still_click_uac_tip", "依然需要被控端用戶在運行 RustDesk 的 UAC 窗口點擊確認。"), + ("still_click_uac_tip", "依然需要被控端用户在运行 RustDesk 的 UAC 窗口点击确认。"), ("Request Elevation", "请求提权"), ("wait_accept_uac_tip", "请等待远端用户确认 UAC 对话框。"), ("Elevate successfully", "提权成功"), @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "文字聊天"), ("Stop voice call", "停止语音聊天"), ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在ID后面添加/r,或者在卡片选项里选择强制走中继连接。"), - ].iter().cloned().collect(); + ("Reconnect", "重连"), + ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 97a3ebc48..aca4778e6 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index bab81914e..7b959a778 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 05d02dd58..1672af2b9 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Text-Chat"), ("Stop voice call", "Sprachanruf beenden"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 47eeb3367..9c9097f6e 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 4634cea81..dd1322873 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Chat de texto"), ("Stop voice call", "Detener llamada de voz"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 2d0f29a5b..db565fe28 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "گفتگو متنی (چت متنی)"), ("Stop voice call", "توقف تماس صوتی"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 4e0e79aa0..fd46b4cf2 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 09284738a..90c8e105a 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 16c99d207..78648a034 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f4be0396f..d06cc649a 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 15f7b977f..57215e2e5 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Chat testuale"), ("Stop voice call", "Interrompi la chiamata vocale"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index acf1c9b96..6e72d4b04 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index e1bc43182..b7b59ed9c 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 488290537..9fdc29260 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 3b01492d3..2502cb34c 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Handmatig gesloten door de peer"), ("Enable remote configuration modification", "Wijziging configuratie op afstand inschakelen"), ("Run without install", "Uitvoeren zonder installatie"), - ("Always connected via relay", "Altijd verbonden via relay"), + ("Connect via relay", ""), ("Always connect via relay", "Altijd verbinden via relay"), ("whitelist_tip", "Alleen een IP-adres op de witte lijst krijgt toegang tot mijn toestel"), ("Login", "Log In"), @@ -449,5 +449,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Spraakoproep"), ("Text chat", "Tekst chat"), ("Stop voice call", "Stop spraakoproep"), - ].iter().cloned().collect(); + ("relay_hint_tip", ""), + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index e6ba5b171..24563d21f 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index a1ad932b1..078bf3761 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5ece46006..e08700d44 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index e9b83e298..5be2a914a 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index a8ef18d8a..4af362953 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Текстовый чат"), ("Stop voice call", "Завершить голосовой вызов"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 47a795342..bf4b85b1b 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 1eb33b970..f464cb8fc 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 1ade9757a..a6b83d9f3 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index e5704093d..09c34b4fc 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 063892074..2154b2729 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 4190ba399..f46a301f6 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 629c5ac77..93e984be3 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index b683fb78a..214ee83df 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index e4957e3d7..db26e5387 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -446,9 +446,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "幀率"), ("Auto", "自動"), ("Other Default Options", "其它默認選項"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), - ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Voice call", "語音通話"), + ("Text chat", "文字聊天"), + ("Stop voice call", "停止語音聊天"), + ("relay_hint_tip", "可能無法直連,可以嘗試中繼連接。 \n另外,如果想直接使用中繼連接,可以在ID後面添加/r,或者在卡片選項裡選擇強制走中繼連接。"), + ("Reconnect", "重連"), + ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 3c1d7776a..c3894726a 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 76f611429..45c2cc519 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } From 24473ebd7bb8353c21b32d76b95ed8e11ee13050 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 16 Feb 2023 15:01:15 +0800 Subject: [PATCH 080/202] fix: issue #3231 --- flutter/lib/desktop/widgets/remote_menubar.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 0fa12cd6f..2b7f8c00a 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1459,8 +1459,6 @@ class _RemoteMenubarState extends State { if (perms['audio'] != false) { displayMenu .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); - displayMenu - .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); } if (Platform.isWindows && From 9d4f899dfd6df25f6bef117d74593a90f796ba53 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 16 Feb 2023 15:16:54 +0800 Subject: [PATCH 081/202] fix using default onSubmit after tab tapped Signed-off-by: 21pages --- flutter/lib/common.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c01fe8910..0880fdb91 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -632,6 +632,7 @@ class CustomAlertDialog extends StatelessWidget { if (!scopeNode.hasFocus) scopeNode.requestFocus(); }); const double padding = 16; + bool tabTapped = false; return FocusScope( node: scopeNode, autofocus: true, @@ -641,13 +642,15 @@ class CustomAlertDialog extends StatelessWidget { onCancel?.call(); } return KeyEventResult.handled; // avoid TextField exception on escape - } else if (onSubmit != null && + } else if (!tabTapped && + onSubmit != null && key.logicalKey == LogicalKeyboardKey.enter) { if (key is RawKeyDownEvent) onSubmit?.call(); return KeyEventResult.handled; } else if (key.logicalKey == LogicalKeyboardKey.tab) { if (key is RawKeyDownEvent) { scopeNode.nextFocus(); + tabTapped = true; } return KeyEventResult.handled; } From 4cddaa4f0c97906593ce7301f725efb6fd0d86ce Mon Sep 17 00:00:00 2001 From: grummbeer Date: Wed, 15 Feb 2023 16:41:34 +0100 Subject: [PATCH 082/202] Unify button style for desktop --- flutter/lib/common.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0880fdb91..c2f8f9a34 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -500,12 +500,14 @@ class OverlayDialogManager { Offstage( offstage: !showCancel, child: Center( - child: TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate('Cancel'), - style: - const TextStyle(color: MyTheme.accent))))) + child: isDesktop + ? dialogButton('Cancel', onPressed: cancel) + : TextButton( + style: flatButtonStyle, + onPressed: cancel, + child: Text(translate('Cancel'), + style: const TextStyle( + color: MyTheme.accent))))) ])), onCancel: showCancel ? cancel : null, ); From b62a05e15f7e3ad3fc2803dcc60db91cc08467b4 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 16 Feb 2023 07:45:31 +0100 Subject: [PATCH 083/202] CustomDialog. Set padding bottom to default if no actions set --- flutter/lib/common.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c2f8f9a34..85aae4c80 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -662,8 +662,8 @@ class CustomAlertDialog extends StatelessWidget { scrollable: true, title: title, titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0), - contentPadding: EdgeInsets.fromLTRB( - contentPadding ?? padding, 25, contentPadding ?? padding, 10), + contentPadding: EdgeInsets.fromLTRB(contentPadding ?? padding, 25, + contentPadding ?? padding, actions is List ? 10 : padding), content: ConstrainedBox( constraints: contentBoxConstraints, child: Theme( From 891121c64d179db48e117b8c010a0a301f6462aa Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 16 Feb 2023 11:05:07 +0100 Subject: [PATCH 084/202] Unify input labels. Remove colon from login labels --- flutter/lib/common/widgets/login.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 14a2c38bc..43dc3a658 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -324,13 +324,13 @@ class LoginWidgetUserPass extends StatelessWidget { children: [ const SizedBox(height: 8.0), DialogTextField( - title: '${translate("Username")}:', + title: translate("Username"), controller: username, focusNode: userFocusNode, prefixIcon: Icon(Icons.account_circle_outlined), errorText: usernameMsg), DialogTextField( - title: '${translate("Password")}:', + title: translate("Password"), obscureText: true, controller: pass, prefixIcon: Icon(Icons.lock_outline), From 10305ab54809e720eb07a580b03a452346ad1bea Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 16 Feb 2023 20:01:06 +0800 Subject: [PATCH 085/202] refact text clipboard Signed-off-by: fufesou --- src/client.rs | 105 +++++++++++++++++++++++++++++++++-- src/client/io_loop.rs | 106 ++++++++++++------------------------ src/flutter.rs | 28 ++++++++++ src/ui/remote.rs | 5 +- src/ui_session_interface.rs | 45 +++++++++++++-- 5 files changed, 207 insertions(+), 82 deletions(-) diff --git a/src/client.rs b/src/client.rs index 77221bdb2..97012e516 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,7 +3,7 @@ use std::{ net::SocketAddr, ops::{Deref, Not}, str::FromStr, - sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, + sync::{mpsc, Arc, Mutex, RwLock}, }; pub use async_trait::async_trait; @@ -34,7 +34,7 @@ use hbb_common::{ socket_client, sodiumoxide::crypto::{box_, secretbox, sign}, timeout, - tokio::time::Duration, + tokio::{sync::mpsc::UnboundedSender, time::Duration}, AddrMangle, ResultType, Stream, }; pub use helper::LatencyController; @@ -50,21 +50,30 @@ use crate::{ server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, }; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::{ + common::{check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, + ui_session_interface::SessionPermissionConfig, +}; + pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); -pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true); -pub static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); pub const MILLI1: Duration = Duration::from_millis(1); pub const SEC30: Duration = Duration::from_secs(30); /// Client of the remote desktop. pub struct Client; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +struct TextClipboardState { + is_required: bool, + running: bool, +} + #[cfg(not(any(target_os = "android", target_os = "linux")))] lazy_static::lazy_static! { static ref AUDIO_HOST: Host = cpal::default_host(); @@ -73,6 +82,8 @@ lazy_static::lazy_static! { #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); + static ref OLD_CLIPBOARD_TEXT: Arc> = Default::default(); + static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -598,6 +609,86 @@ impl Client { conn.send(&msg_out).await?; Ok(conn) } + + #[inline] + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn set_is_text_clipboard_required(b: bool) { + TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn try_stop_clipboard(_self_id: &str) { + #[cfg(feature = "flutter")] + if crate::flutter::other_sessions_running(_self_id) { + return; + } + TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn try_start_clipboard(_conf_tx: Option<(SessionPermissionConfig, UnboundedSender)>) { + let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); + if clipboard_lock.running { + return; + } + + match ClipboardContext::new() { + Ok(mut ctx) => { + clipboard_lock.running = true; + // ignore clipboard update before service start + check_clipboard(&mut ctx, Some(&OLD_CLIPBOARD_TEXT)); + std::thread::spawn(move || { + log::info!("Start text clipboard loop"); + loop { + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { + break; + } + + if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required { + continue; + } + + if let Some(msg) = check_clipboard(&mut ctx, Some(&OLD_CLIPBOARD_TEXT)) { + #[cfg(feature = "flutter")] + crate::flutter::send_text_clipboard_msg(msg); + #[cfg(not(feature = "flutter"))] + if let Some((cfg, tx)) = &_conf_tx { + if cfg.is_text_clipboard_required() { + let _ = tx.send(Data::Message(msg)); + } + } + } + } + log::info!("Stop text clipboard loop"); + }); + } + Err(err) => { + log::error!("Failed to start clipboard service of client: {}", err); + } + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn get_current_text_clipboard_msg() -> Option { + let txt = &*OLD_CLIPBOARD_TEXT.lock().unwrap(); + if txt.is_empty() { + None + } else { + Some(crate::create_clipboard_msg(txt.clone())) + } + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl TextClipboardState { + fn new() -> Self { + Self { + is_required: true, + running: false, + } + } } /// Audio handler for the [`Client`]. @@ -1148,6 +1239,10 @@ impl LoginConfigHandler { if !name.contains("block-input") { self.save_config(config); } + #[cfg(feature = "flutter")] + if name == "disable-clipboard" { + crate::flutter::update_text_clipboard_required(); + } let mut misc = Misc::new(); misc.set_option(option); let mut msg_out = Message::new(); diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index de91b091d..427d0a72a 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -26,10 +26,10 @@ use hbb_common::{fs, log, Stream}; use crate::client::{ new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, - SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, + SEC30, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; +use crate::common::update_clipboard; use crate::common::{get_default_sound_input, set_sound_input}; use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; @@ -91,7 +91,6 @@ impl Remote { } pub async fn io_loop(&mut self, key: &str, token: &str) { - let stop_clipboard = self.start_clipboard(); let mut last_recv_time = Instant::now(); let mut received = false; let conn_type = if self.handler.is_file_transfer() { @@ -110,9 +109,6 @@ impl Remote { .await { Ok((mut peer, direct)) => { - SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); - SERVER_FILE_TRANSFER_ENABLED.store(true, Ordering::SeqCst); self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready self.handler.set_connection_info(direct, false); @@ -237,12 +233,7 @@ impl Remote { .msgbox("error", "Connection Error", &err.to_string(), ""); } } - if let Some(stop) = stop_clipboard { - stop.send(()).ok(); - } - SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); - SERVER_FILE_TRANSFER_ENABLED.store(false, Ordering::SeqCst); + Client::try_stop_clipboard(&self.handler.id); } fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { @@ -347,46 +338,6 @@ impl Remote { Some(tx) } - fn start_clipboard(&mut self) -> Option> { - if self.handler.is_file_transfer() || self.handler.is_port_forward() { - return None; - } - let (tx, rx) = std::sync::mpsc::channel(); - let old_clipboard = self.old_clipboard.clone(); - let tx_protobuf = self.sender.clone(); - let lc = self.handler.lc.clone(); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - match ClipboardContext::new() { - Ok(mut ctx) => { - // ignore clipboard update before service start - check_clipboard(&mut ctx, Some(&old_clipboard)); - std::thread::spawn(move || loop { - std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); - match rx.try_recv() { - Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { - log::debug!("Exit clipboard service of client"); - break; - } - _ => {} - } - if !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || lc.read().unwrap().disable_clipboard.v - { - continue; - } - if let Some(msg) = check_clipboard(&mut ctx, Some(&old_clipboard)) { - tx_protobuf.send(Data::Message(msg)).ok(); - } - }); - } - Err(err) => { - log::error!("Failed to start clipboard service of client: {}", err); - } - } - Some(tx) - } - async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { match data { Data::Close => { @@ -885,22 +836,28 @@ impl Remote { Some(login_response::Union::PeerInfo(pi)) => { self.handler.handle_peer_info(pi); self.check_clipboard_file_context(); - if !(self.handler.is_file_transfer() - || self.handler.is_port_forward() - || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || self.handler.lc.read().unwrap().disable_clipboard.v) - { - let txt = self.old_clipboard.lock().unwrap().clone(); - if !txt.is_empty() { - let msg_out = crate::create_clipboard_msg(txt); - let sender = self.sender.clone(); - tokio::spawn(async move { - // due to clipboard service interval time - sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; - sender.send(Data::Message(msg_out)).ok(); - }); - } + if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { + let sender = self.sender.clone(); + let permission_config = self.handler.get_permission_config(); + + #[cfg(feature = "flutter")] + Client::try_start_clipboard(None); + #[cfg(not(feature = "flutter"))] + Client::try_start_clipboard(Some(( + permission_config.clone(), + sender.clone(), + ))); + + tokio::spawn(async move { + // due to clipboard service interval time + sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; + if permission_config.is_text_clipboard_required() { + if let Some(msg_out) = Client::get_current_text_clipboard_msg() + { + sender.send(Data::Message(msg_out)).ok(); + } + } + }); } if self.handler.is_file_transfer() { @@ -1092,18 +1049,23 @@ impl Remote { log::info!("Change permission {:?} -> {}", p.permission, p.enabled); match p.permission.enum_value_or_default() { Permission::Keyboard => { - SERVER_KEYBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); + #[cfg(feature = "flutter")] + crate::flutter::update_text_clipboard_required(); + *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("keyboard", p.enabled); } Permission::Clipboard => { - SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); + #[cfg(feature = "flutter")] + crate::flutter::update_text_clipboard_required(); + *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("clipboard", p.enabled); } Permission::Audio => { self.handler.set_permission("audio", p.enabled); } Permission::File => { - SERVER_FILE_TRANSFER_ENABLED.store(p.enabled, Ordering::SeqCst); + *self.handler.server_file_transfer_enabled.write().unwrap() = + p.enabled; if !p.enabled && self.handler.is_file_transfer() { return true; } @@ -1416,7 +1378,7 @@ impl Remote { fn check_clipboard_file_context(&self) { #[cfg(windows)] { - let enabled = SERVER_FILE_TRANSFER_ENABLED.load(Ordering::SeqCst) + let enabled = *self.handler.server_file_transfer_enabled.read().unwrap() && self.handler.lc.read().unwrap().enable_file_transfer.v; ContextSend::enable(enabled); } diff --git a/src/flutter.rs b/src/flutter.rs index bd1f4f1af..c8f875da5 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -464,6 +464,9 @@ pub fn session_add( let session: Session = Session { id: session_id.clone(), + server_keyboard_enabled: Arc::new(RwLock::new(true)), + server_file_transfer_enabled: Arc::new(RwLock::new(true)), + server_clipboard_enabled: Arc::new(RwLock::new(true)), ..Default::default() }; @@ -514,6 +517,31 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy } } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn update_text_clipboard_required() { + let is_required = SESSIONS + .read() + .unwrap() + .iter() + .any(|(_id, session)| session.is_text_clipboard_required()); + Client::set_is_text_clipboard_required(is_required); +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn other_sessions_running(id: &str) -> bool { + SESSIONS.read().unwrap().keys().filter(|k| *k != id).count() != 0 +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn send_text_clipboard_msg(msg: Message) { + for (_id, session) in SESSIONS.read().unwrap().iter() { + if session.is_text_clipboard_required() { + session.send(Data::Message(msg.clone())); + } + } +} + // Server Side #[cfg(not(any(target_os = "ios")))] pub mod connection_manager { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 1725a8f41..a86f07d0f 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, RwLock}, }; use sciter::{ @@ -454,6 +454,9 @@ impl SciterSession { id: id.clone(), password: password.clone(), args, + server_keyboard_enabled: Arc::new(RwLock::new(true)), + server_file_transfer_enabled: Arc::new(RwLock::new(true)), + server_clipboard_enabled: Arc::new(RwLock::new(true)), ..Default::default() }; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 97db904d4..947f8fb6f 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,9 +1,11 @@ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::Duration; +use std::sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, Mutex, RwLock, +}; +use std::time::{Duration, SystemTime}; use async_trait::async_trait; use bytes::Bytes; @@ -37,9 +39,38 @@ pub struct Session { pub sender: Arc>>>, pub thread: Arc>>>, pub ui_handler: T, + pub server_keyboard_enabled: Arc>, + pub server_file_transfer_enabled: Arc>, + pub server_clipboard_enabled: Arc>, +} + +#[derive(Clone)] +pub struct SessionPermissionConfig { + pub lc: Arc>, + pub server_keyboard_enabled: Arc>, + pub server_file_transfer_enabled: Arc>, + pub server_clipboard_enabled: Arc>, +} + +impl SessionPermissionConfig { + pub fn is_text_clipboard_required(&self) -> bool { + println!("REMOVE ME ==================== is_text_clipboard_required {} -{}-{}", *self.server_clipboard_enabled.read().unwrap(), *self.server_keyboard_enabled.read().unwrap(), !self.lc.read().unwrap().disable_clipboard.v); + *self.server_clipboard_enabled.read().unwrap() + && *self.server_keyboard_enabled.read().unwrap() + && !self.lc.read().unwrap().disable_clipboard.v + } } impl Session { + pub fn get_permission_config(&self) -> SessionPermissionConfig { + SessionPermissionConfig { + lc: self.lc.clone(), + server_keyboard_enabled: self.server_keyboard_enabled.clone(), + server_file_transfer_enabled: self.server_file_transfer_enabled.clone(), + server_clipboard_enabled: self.server_clipboard_enabled.clone(), + } + } + pub fn is_file_transfer(&self) -> bool { self.lc .read() @@ -128,6 +159,12 @@ impl Session { self.lc.read().unwrap().is_privacy_mode_supported() } + pub fn is_text_clipboard_required(&self) -> bool { + *self.server_clipboard_enabled.read().unwrap() + && *self.server_keyboard_enabled.read().unwrap() + && !self.lc.read().unwrap().disable_clipboard.v + } + pub fn refresh_video(&self) { self.send(Data::Message(LoginConfigHandler::refresh())); } @@ -445,7 +482,7 @@ impl Session { KeyRelease(key) }; let event = Event { - time: std::time::SystemTime::now(), + time: SystemTime::now(), unicode: None, code: keycode as _, scan_code: scancode as _, From 241925dc83c7b656e92171977eb1676c2c0e1908 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 16 Feb 2023 20:28:06 +0800 Subject: [PATCH 086/202] remove debug print Signed-off-by: fufesou --- src/ui_session_interface.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 947f8fb6f..2344f84a1 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -54,7 +54,6 @@ pub struct SessionPermissionConfig { impl SessionPermissionConfig { pub fn is_text_clipboard_required(&self) -> bool { - println!("REMOVE ME ==================== is_text_clipboard_required {} -{}-{}", *self.server_clipboard_enabled.read().unwrap(), *self.server_keyboard_enabled.read().unwrap(), !self.lc.read().unwrap().disable_clipboard.v); *self.server_clipboard_enabled.read().unwrap() && *self.server_keyboard_enabled.read().unwrap() && !self.lc.read().unwrap().disable_clipboard.v From 0d2113cd293446317ec1f64a263347615070ae0c Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 16 Feb 2023 20:48:42 +0800 Subject: [PATCH 087/202] build android Signed-off-by: fufesou --- src/client.rs | 5 ++++- src/client/io_loop.rs | 6 ++++++ src/flutter_ffi.rs | 4 +++- src/keyboard.rs | 4 +++- src/ui_session_interface.rs | 1 + 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 97012e516..51e7f9a29 100644 --- a/src/client.rs +++ b/src/client.rs @@ -18,6 +18,8 @@ use sha2::{Digest, Sha256}; use uuid::Uuid; pub use file_trait::FileManager; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::tokio::sync::mpsc::UnboundedSender; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -34,7 +36,7 @@ use hbb_common::{ socket_client, sodiumoxide::crypto::{box_, secretbox, sign}, timeout, - tokio::{sync::mpsc::UnboundedSender, time::Duration}, + tokio::time::Duration, AddrMangle, ResultType, Stream, }; pub use helper::LatencyController; @@ -1240,6 +1242,7 @@ impl LoginConfigHandler { self.save_config(config); } #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] if name == "disable-clipboard" { crate::flutter::update_text_clipboard_required(); } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 427d0a72a..c673531ec 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -233,6 +233,7 @@ impl Remote { .msgbox("error", "Connection Error", &err.to_string(), ""); } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] Client::try_stop_clipboard(&self.handler.id); } @@ -841,13 +842,16 @@ impl Remote { let permission_config = self.handler.get_permission_config(); #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Client::try_start_clipboard(None); #[cfg(not(feature = "flutter"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Client::try_start_clipboard(Some(( permission_config.clone(), sender.clone(), ))); + #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { // due to clipboard service interval time sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; @@ -1050,12 +1054,14 @@ impl Remote { match p.permission.enum_value_or_default() { Permission::Keyboard => { #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::flutter::update_text_clipboard_required(); *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("keyboard", p.enabled); } Permission::Clipboard => { #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::flutter::update_text_clipboard_required(); *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("clipboard", p.enabled); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0aa7de07f..f3bc45856 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,11 +1,13 @@ use crate::{ client::file_trait::FileManager, common::make_fd_to_json, - common::{get_default_sound_input, is_keyboard_mode_supported}, + common::is_keyboard_mode_supported, flutter::{self, SESSIONS}, flutter::{session_add, session_start_}, ui_interface::{self, *}, }; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::get_default_sound_input; use flutter_rust_bridge::{StreamSink, SyncReturn}; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, diff --git a/src/keyboard.rs b/src/keyboard.rs index 4dcbe5c97..3f7ed6779 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -5,7 +5,9 @@ use crate::common::GrabState; use crate::flutter::{CUR_SESSION_ID, SESSIONS}; #[cfg(not(any(feature = "flutter", feature = "cli")))] use crate::ui::CUR_SESSION; -use hbb_common::{log, message_proto::*}; +use hbb_common::message_proto::*; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::log; use rdev::{Event, EventType, Key}; #[cfg(any(target_os = "windows", target_os = "macos"))] use std::sync::atomic::{AtomicBool, Ordering}; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2344f84a1..b225151ff 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,3 +1,4 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::str::FromStr; From 4cd36e9bd0b3405313f20d67e7da8d71366cc370 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 16 Feb 2023 16:23:46 +0100 Subject: [PATCH 088/202] Unify password field behavior --- .../desktop/pages/desktop_setting_page.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 378ddbd1b..25c485a2a 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1832,6 +1832,7 @@ void changeSocks5Proxy() async { var proxyController = TextEditingController(text: proxy); var userController = TextEditingController(text: username); var pwdController = TextEditingController(text: password); + RxBool obscure = true.obs; var isInProgress = false; gFFI.dialogManager.show((setState, close) { @@ -1929,12 +1930,17 @@ void changeSocks5Proxy() async { width: 24.0, ), Expanded( - child: TextField( - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), - controller: pwdController, - ), + child: Obx(() => TextField( + obscureText: obscure.value, + decoration: InputDecoration( + border: const OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: () => obscure.value = !obscure.value, + icon: Icon(obscure.value + ? Icons.visibility_off + : Icons.visibility))), + controller: pwdController, + )), ), ], ), From 6432183bb4ff58776ad8682c9e8e55200609d1cf Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Thu, 16 Feb 2023 16:44:39 +0100 Subject: [PATCH 089/202] Update es.rs New terms added --- src/lang/es.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index dd1322873..63c1d26fc 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -449,7 +449,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Llamada de voz"), ("Text chat", "Chat de texto"), ("Stop voice call", "Detener llamada de voz"), - ("relay_hint_tip", ""), - ("Reconnect", ""), + ("relay_hint_tip", "Puede que no sea posible conectar directamente. Puedes tratar de conectar a través de relay. \nAdicionalmente, si quieres usar relay en el primer intento, puedes añadir el sufijo \"/r\" a la ID o seleccionar la opción \"Conectar siempre a través de relay\" en la tarjeta del par."), + ("Reconnect", "Reconectar"), ].iter().cloned().collect(); } From a0caf8f257d43bc83df8edb6c17d5d2a3ec3ae1d Mon Sep 17 00:00:00 2001 From: ilGigioVr88 Date: Thu, 16 Feb 2023 17:15:37 +0100 Subject: [PATCH 090/202] Update it.rs --- src/lang/it.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 57215e2e5..ab0c8064c 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -450,6 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Chat testuale"), ("Stop voice call", "Interrompi la chiamata vocale"), ("relay_hint_tip", ""), - ("Reconnect", ""), + ("Reconnect", "Riconnetti"), ].iter().cloned().collect(); } From 897f694ad4a76d35cac57891eddb5204eebcd1ce Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Thu, 16 Feb 2023 18:17:42 +0100 Subject: [PATCH 091/202] fix for #3240 --- flutter/assets/actions_mobile.svg | 2 ++ flutter/lib/desktop/widgets/remote_menubar.dart | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 flutter/assets/actions_mobile.svg diff --git a/flutter/assets/actions_mobile.svg b/flutter/assets/actions_mobile.svg new file mode 100644 index 000000000..6aed6053e --- /dev/null +++ b/flutter/assets/actions_mobile.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 2b7f8c00a..3bec6862a 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -413,14 +413,18 @@ class _RemoteMenubarState extends State { menubarItems.add(_buildPinMenubar(context)); menubarItems.add(_buildFullscreen(context)); if (widget.ffi.ffiModel.isPeerAndroid) { - menubarItems.add(IconButton( + menubarItems.add(MenuButton( tooltip: translate('Mobile Actions'), - color: _MenubarTheme.blueColor, - icon: const Icon(Icons.build), + icon: SvgPicture.asset( + "assets/actions_mobile.svg", + color: Colors.white, + ), onPressed: () { widget.ffi.dialogManager .toggleMobileActionsOverlay(ffi: widget.ffi); }, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, )); } } From 285b5033165f48b802852d1eba16f97c7b0fd377 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 16 Feb 2023 19:20:26 +0100 Subject: [PATCH 092/202] improve input of permanent password --- .../lib/desktop/pages/desktop_home_page.dart | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index d9afbea55..b5cadbcdf 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -596,13 +596,13 @@ void setPasswordDialog() async { }); final pass = p0.text.trim(); if (pass.isNotEmpty) { - for (var r in rules) { - if (!r.validate(pass)) { - setState(() { - errMsg0 = '${translate('Prompt')}: ${r.name}'; - }); - return; - } + final Iterable violations = rules.where((r) => !r.validate(pass)); + if (violations.isNotEmpty) { + setState(() { + errMsg0 = + '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}'; + }); + return; } } if (p1.text.trim() != pass) { @@ -639,6 +639,9 @@ void setPasswordDialog() async { autofocus: true, onChanged: (value) { rxPass.value = value.trim(); + setState(() { + errMsg0 = ''; + }); }, ), ), @@ -662,6 +665,11 @@ void setPasswordDialog() async { labelText: translate('Confirmation'), errorText: errMsg1.isNotEmpty ? errMsg1 : null), controller: p1, + onChanged: (value) { + setState(() { + errMsg1 = ''; + }); + }, ), ), ], From 512563f7967182918f67fc1d8c435c7e2959f987 Mon Sep 17 00:00:00 2001 From: solokot Date: Fri, 17 Feb 2023 02:08:02 +0300 Subject: [PATCH 093/202] update ru.rs --- src/lang/ru.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 4af362953..c389d6821 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -209,8 +209,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Закрыто удалённым узлом вручную"), ("Enable remote configuration modification", "Разрешить удалённое изменение конфигурации"), ("Run without install", "Запустить без установки"), - ("Connect via relay", ""), - ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), + ("Connect via relay", "Подключится через ретранслятор"), + ("Always connect via relay", "Всегда подключаться через ретранслятор"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("Login", "Войти"), ("Verify", "Проверить"), @@ -449,7 +449,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Голосовой вызов"), ("Text chat", "Текстовый чат"), ("Stop voice call", "Завершить голосовой вызов"), - ("relay_hint_tip", ""), - ("Reconnect", ""), + ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."), + ("Reconnect", "Переподключить"), ].iter().cloned().collect(); } From 000799d1814e7d854f53ce5c51aad0829c7aebbf Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 17 Feb 2023 11:59:03 +0800 Subject: [PATCH 094/202] fix CI --- .github/workflows/flutter-ci.yml | 2 +- .github/workflows/flutter-nightly.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 5d4cf39c9..78c60df37 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -105,7 +105,7 @@ jobs: - name: Install build runtime run: | - brew install llvm create-dmg nasm yasm cmake gcc wget ninja + brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config - name: Install flutter uses: subosito/flutter-action@v2 diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 1ab21dbff..ffcadd18b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -183,7 +183,7 @@ jobs: - name: Install build runtime run: | - brew install llvm create-dmg nasm yasm cmake gcc wget ninja + brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config - name: Install flutter uses: subosito/flutter-action@v2 From 302499d1e01babe5d7eb147b07407e1bbfd3d4d5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 17 Feb 2023 13:32:17 +0800 Subject: [PATCH 095/202] fix sync displays info && select monitor menu Signed-off-by: fufesou --- .../widgets/material_mod_popup_menu.dart | 2 +- flutter/lib/desktop/widgets/menu_button.dart | 6 +- .../lib/desktop/widgets/remote_menubar.dart | 86 ++++++++++--------- flutter/lib/models/model.dart | 24 ++++++ flutter/lib/models/state_model.dart | 1 + libs/hbb_common/protos/message.proto | 1 + src/client/io_loop.rs | 8 ++ src/common.rs | 2 + src/flutter.rs | 33 ++++--- src/server/connection.rs | 84 ++++++++++-------- src/server/video_service.rs | 52 +++++++++-- src/ui/header.tis | 8 ++ src/ui/remote.rs | 34 +++++--- src/ui_session_interface.rs | 1 + 14 files changed, 234 insertions(+), 108 deletions(-) diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 47de1be20..3e85cb296 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -1400,7 +1400,7 @@ class PopupMenuButtonState extends State> { } return MenuButton( - icon: widget.icon ?? Icon(Icons.adaptive.more), + child: widget.icon ?? Icon(Icons.adaptive.more), tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: widget.enabled ? showButtonMenu : null, diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index 7c9fe67eb..96cc9fa9b 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -5,7 +5,7 @@ class MenuButton extends StatefulWidget { final Color color; final Color hoverColor; final Color? splashColor; - final Widget icon; + final Widget child; final String? tooltip; final EdgeInsetsGeometry padding; final bool enableFeedback; @@ -14,7 +14,7 @@ class MenuButton extends StatefulWidget { required this.onPressed, required this.color, required this.hoverColor, - required this.icon, + required this.child, this.splashColor, this.tooltip = "", this.padding = const EdgeInsets.symmetric(horizontal: 3, vertical: 6), @@ -51,7 +51,7 @@ class _MenuButtonState extends State { splashColor: widget.splashColor, enableFeedback: widget.enableFeedback, onTap: widget.onPressed, - child: widget.icon, + child: widget.child, ), ), ), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 2b7f8c00a..c97ef9d32 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -472,7 +472,7 @@ class _RemoteMenubarState extends State { onPressed: () { widget.state.switchPin(); }, - icon: SvgPicture.asset( + child: SvgPicture.asset( pin ? "assets/pinned.svg" : "assets/unpinned.svg", color: Colors.white, ), @@ -488,7 +488,7 @@ class _RemoteMenubarState extends State { onPressed: () { _setFullscreen(!isFullscreen); }, - icon: SvgPicture.asset( + child: SvgPicture.asset( isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", color: Colors.white, ), @@ -499,7 +499,7 @@ class _RemoteMenubarState extends State { Widget _buildMonitor(BuildContext context) { final pi = widget.ffi.ffiModel.pi; - return mod_menu.PopupMenuButton( + final monitor = mod_menu.PopupMenuButton( tooltip: translate('Select Monitor'), position: mod_menu.PopupMenuPosition.under, icon: Stack( @@ -524,43 +524,44 @@ class _RemoteMenubarState extends State { itemBuilder: (BuildContext context) { final List rowChildren = []; for (int i = 0; i < pi.displays.length; i++) { - rowChildren.add( - Stack( - alignment: Alignment.center, - children: [ - SvgPicture.asset( - "assets/display.svg", - color: Colors.white, - ), - TextButton( - child: Container( - alignment: AlignmentDirectional.center, - constraints: - const BoxConstraints(minHeight: _MenubarTheme.height), - child: Padding( - padding: const EdgeInsets.only(bottom: 2.5), - child: Text( - (i + 1).toString(), - style: TextStyle( - color: Colors.white, - ), + rowChildren.add(MenuButton( + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + child: Container( + alignment: AlignmentDirectional.center, + constraints: + const BoxConstraints(minHeight: _MenubarTheme.height), + child: Stack( + alignment: Alignment.center, + children: [ + SvgPicture.asset( + "assets/display.svg", + color: Colors.white, + ), + Padding( + padding: const EdgeInsets.only(bottom: 2.5), + child: Text( + (i + 1).toString(), + style: TextStyle( + color: Colors.white, + fontSize: 12, ), ), - ), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - _menuDismissCallback(); - } - RxInt display = CurrentDisplayState.find(widget.id); - if (display.value != i) { - bind.sessionSwitchDisplay(id: widget.id, value: i); - } - }, - ) - ], + ) + ], + ), ), - ); + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + _menuDismissCallback(); + } + RxInt display = CurrentDisplayState.find(widget.id); + if (display.value != i) { + bind.sessionSwitchDisplay(id: widget.id, value: i); + } + }, + )); } return >[ mod_menu.PopupMenuItem( @@ -576,6 +577,11 @@ class _RemoteMenubarState extends State { ]; }, ); + + return Obx(() => Offstage( + offstage: stateGlobal.displaysCount.value < 2, + child: monitor, + )); } Widget _buildControl(BuildContext context) { @@ -674,7 +680,7 @@ class _RemoteMenubarState extends State { ? translate('Stop session recording') : translate('Start session recording'), onPressed: () => value.toggle(), - icon: SvgPicture.asset( + child: SvgPicture.asset( "assets/rec.svg", color: Colors.white, ), @@ -697,7 +703,7 @@ class _RemoteMenubarState extends State { onPressed: () { clientClose(widget.id, widget.ffi.dialogManager); }, - icon: SvgPicture.asset( + child: SvgPicture.asset( "assets/close.svg", color: Colors.white, ), @@ -767,7 +773,7 @@ class _RemoteMenubarState extends State { return tooltipText == null ? const Offstage() : MenuButton( - icon: _getVoiceCallIcon(), + child: _getVoiceCallIcon(), tooltip: translate(tooltipText), onPressed: () => bind.sessionCloseVoiceCall(id: widget.id), color: _MenubarTheme.redColor, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 458ca29f4..1afb5b147 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -140,6 +140,8 @@ class FfiModel with ChangeNotifier { handleMsgBox(evt, peerId); } else if (name == 'peer_info') { handlePeerInfo(evt, peerId); + } else if (name == 'sync_peer_info') { + handleSyncPeerInfo(evt, peerId); } else if (name == 'connection_ready') { setConnectionType( peerId, evt['secure'] == 'true', evt['direct'] == 'true'); @@ -415,6 +417,7 @@ class FfiModel with ChangeNotifier { d.cursorEmbedded = d0['cursor_embedded'] == 1; _pi.displays.add(d); } + stateGlobal.displaysCount.value = _pi.displays.length; if (_pi.currentDisplay < _pi.displays.length) { _display = _pi.displays[_pi.currentDisplay]; } @@ -431,6 +434,27 @@ class FfiModel with ChangeNotifier { notifyListeners(); } + /// Handle the peer info synchronization event based on [evt]. + handleSyncPeerInfo(Map evt, String peerId) async { + if (evt['displays'] != null) { + List displays = json.decode(evt['displays']); + List newDisplays = []; + for (int i = 0; i < displays.length; ++i) { + Map d0 = displays[i]; + var d = Display(); + d.x = d0['x'].toDouble(); + d.y = d0['y'].toDouble(); + d.width = d0['width']; + d.height = d0['height']; + d.cursorEmbedded = d0['cursor_embedded'] == 1; + newDisplays.add(d); + } + _pi.displays = newDisplays; + stateGlobal.displaysCount.value = _pi.displays.length; + } + notifyListeners(); + } + updateBlockInputState(Map evt, String peerId) { _inputBlocked = evt['input_state'] == 'on'; notifyListeners(); diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index e4c9fa03f..761c95ded 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -14,6 +14,7 @@ class StateGlobal { final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteMenuBar = false.obs; + final RxInt displaysCount = 0.obs; int get windowId => _windowId; bool get fullscreen => _fullscreen; diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 7e3d0b0a4..2a3fd05b4 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -636,5 +636,6 @@ message Message { SwitchSidesResponse switch_sides_response = 22; VoiceCallRequest voice_call_request = 23; VoiceCallResponse voice_call_response = 24; + PeerInfo peer_info = 25; } } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index c673531ec..b51c481a5 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1253,6 +1253,14 @@ impl Remote { } } } + Some(message::Union::PeerInfo(pi)) => { + match pi.conn_id { + crate::SYNC_PEER_INFO_DISPLAYS => { + self.handler.set_displays(&pi.displays); + } + _ => {} + } + } _ => {} } } diff --git a/src/common.rs b/src/common.rs index ee44cf4f2..02d367b5e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -37,6 +37,8 @@ pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future) -> String { + let mut msg_vec = Vec::new(); + for ref d in displays.iter() { + let mut h: HashMap<&str, i32> = Default::default(); + h.insert("x", d.x); + h.insert("y", d.y); + h.insert("width", d.width); + h.insert("height", d.height); + h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 }); + msg_vec.push(h); + } + serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) + } } impl InvokeUiSession for FlutterHandler { @@ -316,17 +330,7 @@ impl InvokeUiSession for FlutterHandler { } fn set_peer_info(&self, pi: &PeerInfo) { - let mut displays = Vec::new(); - for ref d in pi.displays.iter() { - let mut h: HashMap<&str, i32> = Default::default(); - h.insert("x", d.x); - h.insert("y", d.y); - h.insert("width", d.width); - h.insert("height", d.height); - h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 }); - displays.push(h); - } - let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); + let displays = Self::make_displays_msg(&pi.displays); let mut features: HashMap<&str, i32> = Default::default(); for ref f in pi.features.iter() { features.insert("privacy_mode", if f.privacy_mode { 1 } else { 0 }); @@ -351,6 +355,13 @@ impl InvokeUiSession for FlutterHandler { ); } + fn set_displays(&self, displays: &Vec) { + self.push_event( + "sync_peer_info", + vec![("displays", &Self::make_displays_msg(displays))], + ); + } + fn on_connected(&self, _conn_type: ConnType) {} fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { diff --git a/src/server/connection.rs b/src/server/connection.rs index 53ccd7008..1a974c51d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -6,7 +6,10 @@ use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; use crate::{ - client::{start_audio_thread, LatencyController, MediaData, MediaSender, new_voice_call_request, new_voice_call_response}, + client::{ + new_voice_call_request, new_voice_call_response, start_audio_thread, LatencyController, + MediaData, MediaSender, + }, common::{get_default_sound_input, set_sound_input}, video_service, }; @@ -672,15 +675,15 @@ impl Connection { .collect(); if !whitelist.is_empty() && whitelist - .iter() - .filter(|x| x == &"0.0.0.0") - .next() - .is_none() + .iter() + .filter(|x| x == &"0.0.0.0") + .next() + .is_none() && whitelist - .iter() - .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) - .next() - .is_none() + .iter() + .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) + .next() + .is_none() { self.send_login_error("Your ip is blocked by the peer") .await; @@ -806,7 +809,7 @@ impl Connection { }; self.post_conn_audit(json!({"peer": self.peer_info, "type": conn_type})); #[allow(unused_mut)] - let mut username = crate::platform::get_active_username(); + let mut username = crate::platform::get_active_username(); let mut res = LoginResponse::new(); let mut pi = PeerInfo { username: username.clone(), @@ -833,7 +836,7 @@ impl Connection { h265, ..Default::default() }) - .into(); + .into(); } if self.port_forward_socket.is_some() { @@ -877,7 +880,7 @@ impl Connection { privacy_mode: video_service::is_privacy_mode_supported(), ..Default::default() }) - .into(); + .into(); let mut sub_service = false; if self.file_transfer.is_some() { @@ -893,10 +896,11 @@ impl Connection { res.set_error(format!("{}", err)); } Ok((current, displays)) => { - pi.displays = displays.into(); + pi.displays = displays.clone(); pi.current_display = current as _; res.set_peer_info(pi); sub_service = true; + *super::video_service::LAST_SYNC_DISPLAYS.write().unwrap() = displays; } } } @@ -1160,7 +1164,7 @@ impl Connection { "Failed to access remote {}, please make sure if it is open", addr )) - .await; + .await; return false; } } @@ -1324,12 +1328,12 @@ impl Connection { } } Some(message::Union::Clipboard(cb)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.clipboard { - update_clipboard(cb, None); - } + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.clipboard { + update_clipboard(cb, None); } + } Some(message::Union::Cliprdr(_clip)) => { if self.file_transfer_enabled() { #[cfg(windows)] @@ -1512,15 +1516,15 @@ impl Connection { } Some(misc::Union::RestartRemoteDevice(_)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.restart { - match system_shutdown::reboot() { - Ok(_) => log::info!("Restart by the peer"), - Err(e) => log::error!("Failed to restart:{}", e), - } + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.restart { + match system_shutdown::reboot() { + Ok(_) => log::info!("Restart by the peer"), + Err(e) => log::error!("Failed to restart:{}", e), } } + } Some(misc::Union::ElevationRequest(r)) => match r.union { Some(elevation_request::Union::Direct(_)) => { #[cfg(windows)] @@ -1530,8 +1534,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Direct, ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1549,8 +1553,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Logon(_r.username, _r.password), ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1571,7 +1575,11 @@ impl Connection { // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. latency_controller.lock().unwrap().set_audio_only(true); self.audio_sender = Some(start_audio_thread(Some(latency_controller))); - allow_err!(self.audio_sender.as_ref().unwrap().send(MediaData::AudioFormat(format))); + allow_err!(self + .audio_sender + .as_ref() + .unwrap() + .send(MediaData::AudioFormat(format))); } } #[cfg(feature = "flutter")] @@ -1583,7 +1591,7 @@ impl Connection { "--switch_uuid", uuid.to_string().as_ref(), ]) - .ok(); + .ok(); self.send_close_reason_no_retry("Closed as expected").await; self.on_close("switch sides", false).await; return false; @@ -1596,7 +1604,9 @@ impl Connection { if let Some(sender) = &self.audio_sender { allow_err!(sender.send(MediaData::AudioFrame(frame))); } else { - log::warn!("Processing audio frame without the voice call audio sender."); + log::warn!( + "Processing audio frame without the voice call audio sender." + ); } } } @@ -1646,7 +1656,9 @@ impl Connection { pub async fn close_voice_call(&mut self) { // Restore to the prior audio device. - if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { + if let Some(sound_input) = + std::mem::replace(&mut self.audio_input_device_before_voice_call, None) + { set_sound_input(sound_input); } // Notify the connection manager that the voice call has been closed. @@ -1821,13 +1833,13 @@ impl Connection { lock_screen().await; } #[cfg(not(any(target_os = "android", target_os = "ios")))] - let data = if self.chat_unanswered { + let data = if self.chat_unanswered { ipc::Data::Disconnected } else { ipc::Data::Close }; #[cfg(any(target_os = "android", target_os = "ios"))] - let data = ipc::Data::Close; + let data = ipc::Data::Close; self.tx_to_cm.send(data).ok(); self.port_forward_socket.take(); } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index bc9c5ff6f..52b1717c4 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -65,6 +65,7 @@ lazy_static::lazy_static! { pub static ref VIDEO_QOS: Arc> = Default::default(); pub static ref IS_UAC_RUNNING: Arc> = Default::default(); pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc> = Default::default(); + pub static ref LAST_SYNC_DISPLAYS: Arc>> = Default::default(); } fn is_capturer_mag_supported() -> bool { @@ -407,6 +408,43 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType Option> { + let displays = try_get_displays().ok()?; + let last_sync_displays = &*LAST_SYNC_DISPLAYS.read().unwrap(); + + if displays.len() != last_sync_displays.len() { + Some(displays) + } else { + for i in 0..displays.len() { + if displays[i].height() != (last_sync_displays[i].height as usize) { + return Some(displays); + } + if displays[i].width() != (last_sync_displays[i].width as usize) { + return Some(displays); + } + if displays[i].origin() != (last_sync_displays[i].x, last_sync_displays[i].y) { + return Some(displays); + } + } + None + } +} + +fn check_displays_changed() -> Option { + let displays = check_displays_new()?; + let (current, displays) = get_displays_2(&displays); + let mut pi = PeerInfo { + conn_id: crate::SYNC_PEER_INFO_DISPLAYS, + ..Default::default() + }; + pi.displays = displays.clone(); + pi.current_display = current as _; + let mut msg_out = Message::new(); + msg_out.set_peer_info(pi); + *LAST_SYNC_DISPLAYS.write().unwrap() = displays; + Some(msg_out) +} + fn run(sp: GenericService) -> ResultType<()> { #[cfg(windows)] ensure_close_virtual_device()?; @@ -529,6 +567,11 @@ fn run(sp: GenericService) -> ResultType<()> { let now = time::Instant::now(); if last_check_displays.elapsed().as_millis() > 1000 { last_check_displays = now; + + if let Some(msg_out) = check_displays_changed() { + sp.send(msg_out); + } + if c.ndisplay != get_display_num() { log::info!("Displays changed"); *SWITCH.lock().unwrap() = true; @@ -798,11 +841,7 @@ fn get_display_num() -> usize { } } - if let Ok(d) = try_get_displays() { - d.len() - } else { - 0 - } + LAST_SYNC_DISPLAYS.read().unwrap().len() } pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { @@ -861,6 +900,7 @@ pub async fn switch_display(i: i32) { } } +#[inline] pub fn refresh() { #[cfg(target_os = "android")] Display::refresh_size(); @@ -888,10 +928,12 @@ fn get_primary() -> usize { 0 } +#[inline] pub async fn switch_to_primary() { switch_display(get_primary() as _).await; } +#[inline] #[cfg(not(windows))] fn try_get_displays() -> ResultType> { Ok(Display::all()?) diff --git a/src/ui/header.tis b/src/ui/header.tis index 1fb694397..e25c0d544 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -480,6 +480,14 @@ handler.updatePi = function(v) { } } +handler.updateDisplays = function(v) { + pi.displays = v; + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + function updatePrivacyMode() { var el = $(li#privacy-mode); if (el) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index a86f07d0f..4794efb65 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -53,6 +53,20 @@ impl SciterHandler { allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); } } + + fn make_displays_array(displays: &Vec) -> Value { + let mut displays_value = Value::array(0); + for d in displays.iter() { + let mut display = Value::map(); + display.set_item("x", d.x); + display.set_item("y", d.y); + display.set_item("width", d.width); + display.set_item("height", d.height); + display.set_item("cursor_embedded", d.cursor_embedded); + displays_value.push(display); + } + displays_value + } } impl InvokeUiSession for SciterHandler { @@ -215,22 +229,18 @@ impl InvokeUiSession for SciterHandler { pi_sciter.set_item("hostname", pi.hostname.clone()); pi_sciter.set_item("platform", pi.platform.clone()); pi_sciter.set_item("sas_enabled", pi.sas_enabled); - - let mut displays = Value::array(0); - for ref d in pi.displays.iter() { - let mut display = Value::map(); - display.set_item("x", d.x); - display.set_item("y", d.y); - display.set_item("width", d.width); - display.set_item("height", d.height); - display.set_item("cursor_embedded", d.cursor_embedded); - displays.push(display); - } - pi_sciter.set_item("displays", displays); + pi_sciter.set_item("displays", Self::make_displays_array(&pi.displays)); pi_sciter.set_item("current_display", pi.current_display); self.call("updatePi", &make_args!(pi_sciter)); } + fn set_displays(&self, displays: &Vec) { + self.call( + "updateDisplays", + &make_args!(Self::make_displays_array(displays)), + ); + } + fn on_connected(&self, conn_type: ConnType) { match conn_type { ConnType::RDP => {} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index b225151ff..5a83ee572 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -761,6 +761,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool); fn switch_display(&self, display: &SwitchDisplay); fn set_peer_info(&self, peer_info: &PeerInfo); // flutter + fn set_displays(&self, displays: &Vec); fn on_connected(&self, conn_type: ConnType); fn update_privacy_mode(&self); fn set_permission(&self, name: &str, value: bool); From d95a03924ee2421205390b45574ab2be7e5a7f08 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 17 Feb 2023 13:47:09 +0800 Subject: [PATCH 096/202] fix build Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index d7d944cb2..e82e9d26e 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -415,7 +415,7 @@ class _RemoteMenubarState extends State { if (widget.ffi.ffiModel.isPeerAndroid) { menubarItems.add(MenuButton( tooltip: translate('Mobile Actions'), - icon: SvgPicture.asset( + child: SvgPicture.asset( "assets/actions_mobile.svg", color: Colors.white, ), From 4bff430fdb196d8211d30d8ab9de7b8c923d9b43 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 17 Feb 2023 13:58:16 +0800 Subject: [PATCH 097/202] fix svg warning --- flutter/assets/linux.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg index 5427305ba..1738a02ee 100644 --- a/flutter/assets/linux.svg +++ b/flutter/assets/linux.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + From cdf9867b5c368370c4c9a79c2b5c99bd13a912b6 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 17 Feb 2023 14:33:01 +0800 Subject: [PATCH 098/202] fix update options without auth Signed-off-by: fufesou --- src/server/connection.rs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 1a974c51d..2e2bce3e6 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1092,7 +1092,8 @@ impl Connection { async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) { self.lr = lr.clone(); if let Some(o) = lr.option.as_ref() { - self.update_option(o).await; + // It may not be a good practice to update all options here. + self.update_options(o).await; if let Some(q) = o.video_codec_state.clone().take() { scrap::codec::Encoder::update_video_encoder( self.inner.id(), @@ -1496,7 +1497,7 @@ impl Connection { self.chat_unanswered = true; } Some(misc::Union::Option(o)) => { - self.update_option(&o).await; + self.update_options(&o).await; } Some(misc::Union::RefreshVideo(r)) => { if r { @@ -1665,8 +1666,7 @@ impl Connection { self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } - async fn update_option(&mut self, o: &OptionMessage) { - log::info!("Option update: {:?}", o); + async fn update_options_without_auth(&mut self, o: &OptionMessage) { if let Ok(q) = o.image_quality.enum_value() { let image_quality; if let ImageQuality::NotSet = q { @@ -1691,7 +1691,18 @@ impl Connection { .unwrap() .update_user_fps(o.custom_fps as _); } + if let Some(q) = o.video_codec_state.clone().take() { + scrap::codec::Encoder::update_video_encoder( + self.inner.id(), + scrap::codec::EncoderUpdate::State(q), + ); + } + } + async fn update_options_with_auth(&mut self, o: &OptionMessage) { + if !self.authorized { + return; + } if let Ok(q) = o.lock_after_session_end.enum_value() { if q != BoolOption::NotSet { self.lock_after_session_end = q == BoolOption::Yes; @@ -1818,12 +1829,12 @@ impl Connection { } } } - if let Some(q) = o.video_codec_state.clone().take() { - scrap::codec::Encoder::update_video_encoder( - self.inner.id(), - scrap::codec::EncoderUpdate::State(q), - ); - } + } + + async fn update_options(&mut self, o: &OptionMessage) { + log::info!("Option update: {:?}", o); + self.update_options_without_auth(o); + self.update_options_with_auth(o); } async fn on_close(&mut self, reason: &str, lock: bool) { From 6def4ccdbdf1ea70fae0183a61d52809d17ddb08 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 17 Feb 2023 14:47:42 +0800 Subject: [PATCH 099/202] await Signed-off-by: fufesou --- src/server/connection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 2e2bce3e6..9cdbf974c 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1833,8 +1833,8 @@ impl Connection { async fn update_options(&mut self, o: &OptionMessage) { log::info!("Option update: {:?}", o); - self.update_options_without_auth(o); - self.update_options_with_auth(o); + self.update_options_without_auth(o).await; + self.update_options_with_auth(o).await; } async fn on_close(&mut self, reason: &str, lock: bool) { From 591314617557b283155ddbf124999f2beab9829a Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Fri, 17 Feb 2023 10:44:43 +0100 Subject: [PATCH 100/202] Android adaptive icons and monochromatic icons --- .../android/app/src/main/AndroidManifest.xml | 2 ++ .../com/carriez/flutter_hbb/MainService.kt | 2 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 ++++++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 ++++++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3114 -> 3990 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 7492 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 6161 bytes .../src/main/res/mipmap-hdpi/ic_stat_logo.png | Bin 0 -> 1028 bytes .../src/main/res/mipmap-ldpi/ic_launcher.png | Bin 0 -> 1667 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1939 -> 2207 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 4348 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3525 bytes .../src/main/res/mipmap-mdpi/ic_stat_logo.png | Bin 0 -> 715 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4087 -> 4827 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 9515 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7604 bytes .../main/res/mipmap-xhdpi/ic_stat_logo.png | Bin 0 -> 1524 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 6636 -> 9171 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 33762 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 13879 bytes .../main/res/mipmap-xxhdpi/ic_stat_logo.png | Bin 0 -> 2091 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 8908 -> 9893 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 41583 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16113 bytes .../main/res/mipmap-xxxhdpi/ic_stat_logo.png | Bin 0 -> 3162 bytes .../res/values/ic_launcher_background.xml | 4 ++++ 26 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png create mode 100644 flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/values/ic_launcher_background.xml diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index 04b2ccc9a..9b25f4973 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -16,6 +16,8 @@ + + + + + \ No newline at end of file diff --git a/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..65291b96e --- /dev/null +++ b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file 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 eac2fe7241381b7d162fb15323837ea7101e4a84..d05404d3af59e68e46ab3009c3684052323da15a 100644 GIT binary patch literal 3990 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 AwEzGB 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 new file mode 100644 index 0000000000000000000000000000000000000000..3742f241f4dd449bbf2a5a9fa9289d7e1da2ecb3 GIT binary patch literal 7492 zcmV-K9lPR*P)1iK5lwD@twYj~A~}$=o|~P|>$N-2oH=vm%rnnCpYuFtX08B=#-RYO z0ePbENbaWxq=>ei)Zer^N$uLltw*lb$9nWRkvcBOK|vanu~b%>*+@Nd z+A-6PZ``Fi7pYZ9^Omml!S=)% z3@D&TRFSoml;$a`dC;V?G+EVA$CUHNdZT@7mRwIC^47B1eV~E*)KO2DlRG-a|oja~j-2)z}7e9kzs71$VmfZ+-PBvOa$IYbFM)i5b z$#$S((rC4)x6$z2r1!MC)#}u9eA-k_8v;!$*&JkhMuln=Jf3lZiDr!rkCdT8E5>Th z$w~BC&&6%j{rW~;t7GH2D9+H!IAvG*F)gThJo}C3IPEBB)I5dMJQYURuE?l4ftu~R zvsrX9RbHiC+MiR&Glni8sbE$*&`Zdr|pwb zb9LE-uTz)N^gd=$bL-^{WKnYlM^G;{E8xZVbWkn%}Gh@jG8k{ zIx=d`-~<|>W`CQ@)T~mlh^G^(?`qbX<9oT2H+8*)8Sv$?ESZcl1n^&Z9BKbU9Xl3qGGW}>7UCB2Ar zA#?-1D7t{75kNG+7@~)PK}7FI>%DlTK}JUo@YGL+x%XQ|{(ED_DhZIvGR?b8PinsU z&Q4x;>O%4zD-g~^ScuSD`70oFf-WFB59Cn-XaEmI5%e&^5W-&2yMdkHJw~bY1iu>W z=L4sXw)FfQggHh`n&wMFT7EH2t~S>1;7x$ZE+UPRxG)(=rCZDr@yUHrohMf!d+J#O+8*uhF83GI4i z#<9EAv-0d&yr8Nzu8vtypkH;OcRwSyH1wHBjkYu|=wIwEhPRJ@F{e(Uij zYr{$8Dt~_dK@qA-TXoHKWvtgDPd#SNSKRp1V>$P)*P`Sb2&+N5!wQBqXg|=9STp5n zex1sn80yg}`$eoRsk}ygEptHn85`X~-%tE&(}>R7O13{YQjf=TBjH>N*FG|rj?N2! z51?gsP%gp0igB6uN@YDTJ{@lM?Zm#d{3e2?DxT5Y&p%GAydp$oU9K-Wk4S;}i>6|D z({efKc|=>b(df*|)MDm*#fNXn({a?rz#pTTRlc}{7umN4-$KU?niXk6!QC#QY)rryZ8UU_P;>H~`RePIc?{H18Q zFk(@$!G_`e0weMbil`j%l!V?Z*yI~_lE*Vw%u$a{kptYt*w}yZ!~b%|YfYx?Dm5p0 z(QJOHyi_|!GK|mL=7vtlVNa1afDt?Pg~iTW}J~e2!{6^Me_t+ zX#+c-+*_&OjKNHnoKM&Q?MTfZxvi62=X#LyK=%an2O+ikK}JA}ws9GrD@L64dFJ;Rcj;H_&=&u7AH7uTlK2ytpQ(H_*?ef;SlA4D@JJ_gfK!;1tam2x0L$7bXk zF+AsRWWlpSoe-YfpWHHtGY zUO>L%eniiUKYU=zgh8uW9;_3N+3cAT(FcG5(ESKILH`2r27n>Z!=OEKQa~tx%tP^7 zghhxf0X-Y&E^Ag>eE`sR;}x&vigUK|^#3&G3D;euc>Qw4b3ga^$IrL57d7wadeM3# z1o-^GY&yFxLGhO$Dmo2^VPr|NBp6_9-w}iX(3=qb7+$FluQ)()^Z*C<4YT9nBERVK zc=0*#Jm@_RPFM)Pv$m7t=XB9=R1f+5B9tsecoV`ZgjqnL($8VgkE4t0>3^z^^=n6| zN1sY5ZvI}LxeMpf*>M)at3dyn(%AoFPye%g_^gq-+Hp*7&!~CzdPT?4s}Q*Wjfy9# zjc7MAJ~Sy9gEbAWSuWTMY{b*w;pKe~^W>%>zImO;OZ#e{*PUM&0Y-p9mj3Q8&i?pj z7BBDR6|YO|pbLl^rK9sqUh~=; zS^4%YJbZ7#C8L*<)v_Qi`5WPk=K8n`2QXJXH-+yF)pFPn0{`WoN(f0_X z-}LG7Km4G;3G-JX>Kb5SG)L;+Y;p>@+__x(xjt^b&g0Ob`rB`@tcX&uxvod*)a+IM>;!ijCc`V58O)fqkQ3(HEat47M#c8} zR8VvE8(`(f@~Hf&Ag2V=alu6~T%tCqN)t6e2&h@p$LPrCxaX#A+<8TFgObLOthaZ6 z4CX96ja>d(ki}^Kx`=VUgnIA|vs}Iul|Pjae|kLEt0l5Y#U`2^HBWrZ_`82T2d(}9 zD8FrIj{~(yy%OEmoN+08#LZB;oKMZ^pK!mLgSg(-tzV6BBj{y#L{m_a(=(Is_{KN` z_(R_M&ijD=#P%JNr;3^Kf(DXATsn#we03hpax{g=Q$LU}%!owIraT*wHzl_3plnB~w!tO+nfui%PYfUZ zVJDh*0bSwUPyEmGFO0H0M&-|Akg?)t`Iiq4G_7yVK~!&hc6cBok((@X&cXQ*(Fgvz z22B@Q&f%-y&BxkzPY3NnD$n{lLYPBc2lf!i=EV$S;4qi*5Hdr*F+c zTyMAE=%Mwaz(_n8sla?x&8&l8PXOkzV98wG`mXe2IU=V;%J#O_QS+NGQsg?8qNOKl zTqHz~We`d!KU=7`H1W7(4mL%c(|-}D_>}P zNuQ>Jx)88~@Fo_$p{we#9FV7in$v=z`j9X5pjjLph}&Z#^FsOMMx?Acfab?Meg6>d zAz5#~+;$Mnla!z2AvWx&Nz;Lj=5W+ez9F`pg41NvNIYxZ4QfX9pb1})5AJq5v1{cy zqMmaA&)devYg`%|`bgH>##@SLuaEL0Y0+~v5saBrqG>I2Q5`*1kL7@zG&c0tn?=pF zJZuf98E9rkqT(1eRhFJb*Ipp^Q5@}OsE+3eTZ6dXw*03@acl>gossi0b>JDg@CbEa zM$^bFMD5KTf}~-IQ4LK2k+z{`ccbR=hm$&u^=PUh_86E!$>4jAJ^Xr)D+z2H={kJ*z(KTr zCO)s66d@akMANz(Ep0>1Hr$DtwdzGnS2Q9d^aOz?G^0f;wISYK_V&3-%aEqG=e8H| z^zJY-U{SFdO`9|w8LCJRFsJ6RGHR}gPBi)GLk=cQSJq==MuW0F8RX>`T{XmX9X|ha z4=8`AGZK8&OVb)OorIs`YdHm{$?ks*OjAHAPYG#=zBg>jSnI(B@z4x$z_nSu0O>kB zbPzP*BWha02YZ8sRTe>P9=_!ioF-R`Y~7${Q~^)&5!xwfD|#e>rj2zEC?0N3cP0nP zdXs_*=aegk7DU7BqzDaTHb7`sksb)fNlpOv30{MmbWR80^P?q zwL48jx(<(cXmmczZT9R#*(=WA z*ac1U&`n3W4v%}a0_l!s2`VskVLp=>pKmS_%+9o)g41MWEzNQ_s9Eb>Xni0wth9u7 zQdxzjMa||_Z*bKRAx&?ud3^!YYa@y3M4C4KV$n23JVp}RcSv?WYSs*b95B|SG@bm4 zs7>Xw>7Cb!n@HDTN1+#FaeSAAG*&uIkD_TCYPRXv-qt#5E)TSaqvIb88cOhp81(>< z*V1{6@rwdZAzg=}deKG>F!J`IfuwPBNYnl&P^urR#j;ITzBhD-nms*?$51p96l)0y zuZx;>mXR;aL#4ed)^f!4_Ku4bs$&V71yKsFoTo|CLFS;cdz7XJ@$`XM`;N(Jk+M6~ z?0Lf=`_qFIgO;L?ei^dNMskaJ+h<%HCnoFdt>+hjH=>ytrDBNAqf1ma&%2BPypnH- zEvMi#+5M$nqtkJN1j2`9?QtVB(eh@N{cjiPIazPB7R*7*av&c!u7se? zM?9~+lhMPzA-0@?)8sS{;SM$L`1c{SekhEBu`WW=7%`S}Q^0DvU$vqseQG|UdOQ9^ z$Q8~)%Zi%k8cFK=(R75qA7gOuaO$xfkxdVmWN=BCT55jms~#n9J6eV@=d(}d?`kro3X9GRa;Dd0C7@U`>5R1Z*VM@iv+4xVzpnZqv*p%I; z=7C2%Mt{?fmj0*_no)Ct0S2=6w{?MD#oKRi%@Ct{d-s)!u2-%ARss3o%N4O`S)zaD z_Aq$c&91$>xbHU8*(4vg#d0-w*-C1T8voa3j-uI!mXTPrSc|eFFImYbhhLsRw?pa|`LXtdkM6zKc(vI!fc$tEJQo?of052GHJrqZtTu zW=YYpNz-<@96)m~n{Q~Iw{8wXU0rflFS(BQ0?R}1XXQ_$@1t1W->9c$0L_nR{Cc&N znr%9^x3!L%eH0IGN3%2WeO^14Xn$^`EI&$V>_2$+2d)`n*q@_&&H`Bn!4uWd^MRB| z*_=`F>**bo#O$~{YN4PJO`7x15GDZhIXN$RG|u!Qr|cTyU=j9cEa ztM!C-rM#|>7RLZrKS0d$=vars*y!5LbJoQNH znXFFu#$~&0H{JLhs}52;{C9Y=JL&<=XfcQ$@1#_`h<{tNsZy);6zt0r(f@-T?E0ZQbbQk39-q1=&t*ScN=MfvXt^FOvts9&mI3hBscer#)&l4Q zXzt+KUmRqY({C-tAcVgv0C9tJ&lmwdd3ZUw{NGakS+Oz%zyskyys>Mz`~1y3b9ZC? zUGrJRN&l;p{3{eMY#Zh7KP&R`p62wq_tkL4j|v>!vmCr@5mo>_Wgl6wHf8Xw??;1x zxkAU0Uq~=$x?E>7UU5Ain)5_+Iz4p|qF%8*4Dk(rs#tyV>8RXQ2&aP1Ba8sw#vA(z z-&nPcy^q!B2_>g^-Pv%?zpf;w-jB#>pbMZM2foMX(0%;snf+}3LXoGw)0lUC<2i~o zS9LP;#98FjYLuLda0Wv8%L|e3VQWyqATN@!iITf9CW#oU?#C;w=Y~Jr$j^W5#&v1{ zwnsGKrV;wM@aZmk<}W6vR)BPYcK~m66MG&U;I6er>NS{;KHka9lTIP0{uW^wB87?} z6oLJ~!-)PLL~p|z+lek6q3P6RpfjN0)SvA&q;s5yzMZMmr(nIIYu9L~G}PP=p+N-hVT@B@p42AQ-i7~3V)GwYb!5f^qMNpW{WH1lCbZMhm=Sh#EB@r60>>>r4diOHUKCBrRhEWB z0;20e7}c7yYsL^Ss3ZQgqet+Am1e`{7}3ljn$SeM81`i^=zn2o(Dg6j*0h~!`9m|m zXy+QuX}?yZu>|tke^kskX(f`o23UfY@N*^s66UNUDI&sIjp)=`v>kI!+<%q9OvRWF zWyC%$mvUSp%i9<|d=Npu=40kB2DKi|LLxt~XwfD@e52Uj*KTIIo_ZMcc0_NYIC3M8+%m|_BE*p|0csmjd)Jrf(UpB{;;v3{B=>3R13*L9Y+rr_OwsYetgB&`t(lYg; znju2_7~}YH?0m&rKBHLlp4sHPmLO^cqTd57LFf)2-y)f&?J2v|07e5cgy?4x9svDg zwC>}@?K}APCyG4x=#i(}1~5ITxt4s?;f-`T*#CP#Qpc2Plpl;KiQ}bN_Wk`ZwFh zYUDh9xO^>qG}OoOM$H+f9_^5+7`Kg>6l|Bb6I7ea^}wIunhGT4iR;0*`tz=) z<^9W|<_u22re=HDE^vWfC!3m&Xk^sfdN~7G)SST)*wk!4)X*+)fnJ+Q&92|@P48bu z&8?UHb#Pb5Fv&KMQL`JE_J)|2(5|(QYt-!my^dF&_C^R9HM<}uoex#E+jSecF3mFf zdXHQ8@iJ<557W{R)2P`s>aB%V)9XZK6OGI%H)>GLny+r8e|6Q(sJZp>RKVry2V~Sd zUEq8`WK(mN&~A-9*=QCePZ~06ZoQm=>@lzmj=-j7d)Y2Tgnc&XX71gQ)3+KQ;TRWx2N z>h50_HMd^QK$g(X;0Us)*$sKxBctZl%NfY1IfEmxso7q(3tXVrjG8BaW>T}M*s1iK zF3@X6%@ag3=bBq(h#8Imj5lgdN>qBr-&Df|dd;YL0?4A~*2@{l9s|qZ2yAM$A8Ke9 zxInKNHBSKkSkLx!9_`}|U7**gq-NI*I4ORpiHw@t53XMan_cUH_x}MQbPenh;qAx( O0000yxZ-s@cgAfqT86=9BZr&zDg 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 N002ovPDHLkV1k>~Gu8kA literal 0 HcmV?d00001 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 8c01e98de01749763dc3f6e0557ac66303ceace4..f16b3d61d9817e58e916df8748a8b833981e6b23 100644 GIT binary patch delta 2196 zcmV;F2y6F~51$c`BYyx1a7bBm000ie000ie0hKEb8vpS< z=g#b|cWr|)*aSlfg_Iu!l_;SuDGj7etExfmOVy}K8>MNAsQMQ)MNuE3h>E`Sr4Mar z)L$wMs>DN^1QezGsAvT#B(fl+G+?KcU~FTv{;@wiJNNWqc7MFPv%B84oq+naMt5ds zeCC|aL9m(TsI3-Cb!-=Z$u3+ z@pzo=+qZM@;K5}b9Ua}S>n;+Jx&l;nA@6Va|DNZKkB*KG?b)+uCj@T9a0wWvyFzzOlSaGZh)zCJ}W#6LwU>58`GQ^p5~RCdC~<7js_{h zvB7AR#WCS4-3fMfCfL0!Nq5{KE;Kc%BbZ?!%D|j+{p&ZT`OVcle;xK1FIWnqV5BHb z!AdZykbm}+KMm$MHk#-4(Hu{$XyvQRlFc$uj`=eX3cEPw^U}Zb{OnS$($kIv699ry z0hQpLA{0=@a>|jLS>71UF|>0fabrF-25OyGA)QzDy_w_WpjYM#Da+1U90gR1GrGhC zC`N_Nt#KUW&N6V}md~?i^1OPjP}}1KN`|*kgMYOec%;aLF~U>#q}aEvof>nx8O)!7 zUtaV$b=@y{xWeNgDlAP1JC-{vPa4!HBYDf;$2^8TA5^%%J;uIu?W|6gUT;799Kit~F_AZxJw^EUeue^2Jqd(*G#jZH_ zw0}4N_zLf53w-;+DF4cN{9*G_zR=l1tiByJ7qar|5U5;*Rh6NP^1F8mrC1NcrO3qh z*2nqI`ZyaqoFH@}NIAl1m&WN`0KmbMr@@#ZplXn=`~ zRTJmcD?TR%Ecg^-8IZv9&uotI)6cXte1DlxQeJ`US)a$xkI^^jVFeK3_^oM@PJp1yPeYfEy|o^o<5 z-?W^j7|0fsS1v0BPl6RTtZ|I+qiqQ`bvko4z!l*WtuE?IP%IIwBZY$U=b;=~-+z*H zOqDKCG{wNRG^BHsJYO8UT7=%^W?re#2y9ytqti9PTWv8ZSV()ye{;kl?R@kLq^8}&KRmk+#O zl!q_4Dm=D4PK#4rW;bI%-IaLf#|c;{#;Swx;0obOYYpx^lF=_NP4o7MUzL5@9mCFL z@uuZ8#Q?zWO~RvV15Y^!&wo5UM zx#7M#syu&OxiDCw@75H&a@pcIpvDGaZU__)kI-Gk{l&w}U4F1JF%z=o{n2?D5dG&` z8|{LHZ~k8C8w6{@s((^JgN9bYYOpaDm|4Ch=uRL(7`4TOovR#v`$&?Ggw%cioDHbQ zK)GiM7Ty~XY&dggVmXZMXn%-OccM5xxgo~)x5QYIm`5s`&45M>0JOO9$8W)}|0%q2 z1;(d?IT6Hmv|AC3u7UQL@cA{ulb>|?+PZ~(qMI`VQ9Qa{_ZmA)>EfP$JdMwuZt=6-(o~tq-m&;B1 zzCZ3bmHXUA?SH<|;Ls2OqSaWqvPYd$3(?;_&l}I@^OMo%s+uq9bb2tG&At<@g`#6U zEcAXoOiWB%PN&m@(ch~WIDGi<2RnA`c)qu{HZHX@IKYdEUs>)YL$K zfB)qpM~)P#3hj^E}$x+Q?)wBoYZ!b*2EUl>nmO zb>{+WEtyP){{DXQ`P$8{S_agVMg1@BcNKLRC_|$z>s>XXmU1R?Ky}}!s*iNH@ISfz W5F4|i01bfv0000x+_=N-^sem95At^=)KZKw}K^j^p8_L6mQY_oYw%f

8-hD1K;Q(VnK}SbsK}y9^S@qwPK%x$VPmT|V>#c)&m>2sjLas9@$x_( zm6w7nvtcwpU&9DIlgP*(ACIjL54KS%1AZ5dxg)-q(oM+Rvlv z*8#M+L!!o^AE2IVp0u0e1s^tD1a7!_ET|tq+hAmYDuPe|DCoF`&dtiSU{7&eO4d)H z)5D{(et!^cZXp`H9uhjPc`Cz#6XiL`No)~o6JQH=`3WzsyYvHOOQSf`h?2*Q7;87s z`WDbhi@mu&h$eS{ZZUkH(li1;hk;K#m1+{$-5kU(tsybX@G3J9Ip*37s4cc(MS%qq zQ>!k9p9s(a6AzAUbvwZ-cP02v2Q7$({0zb7-Bj&|$g;gL?TV_Vh zwB&$iwz{zo7y1H-zg!Ql904n{oj6gb*Sa^UkiO!+>$H3{~qh~*c zM7o|rmY)%s0cl_iiA&D=h94JuLz=ZquR$p{p&Coa;WRUpyWAN^ zV4xdROyzL&vYbr+ILz)ii*H_cwnzdw z2zOX=;MCif>U^8xSY-k;fe()W{eDhHqHKZz-@cl>=@o`&pxl3Vnde7zFD;^+fV`Q z{F8&rE8b-GB;fc;4%zn6gYWYRSa-&YosK}{*(p>jznu_nEKXog%%Z&a9RVAD<}f`E z_^uj?;JZn?tB-}y+9%-MQhx)M%rGD~RheX4KaV|*AhtF7;q(f_+f$9jyJ@B!`6=p> z)$EHG7IEb2=kE*%sHzL0(J4OMJ^ybIYfk_(^SE$5VYPxiUUzz^74GKYaPi_J$*8D9 zOb`m`^ZD^cte>I0cy$rbG6ue47caKoF?FJeid~aa0xCqFqayMITYp>c(svWnX&1I2 z4JL!OWD_F5WK+3IfTIJ5dj6@_0IF1VMG_>ZEb7PVE7LGeN9UUmfo1!Frgq3>kOUGF zNp|%MQ&5(pFI6W*;HneKyS7o1cww9g2Vb%2iu_0dk#0%9ZC(Vd_>rSRm!MQ)u1O)u zY?^1mx^nY9H6LRFBY(hNBIm?2>djdko|`OCq2sXYoIty{#7Lx4!CYBl!p6C#MEBV) zMu5EmuZnl%INCRS{TlE&jXc-{9KHyg`bWS}P@9C!B4GaGMpPG4dr)Mg&UaLWv4Iiz zArS(Sr4fKtb3o3)AEJEUAwJn?@26hMCxmNMx|MB|XwyE1T7F`x5mr;un!!U7fwd6d zfRD;7)b7sC2YGV4RTaBJUdITqKTf<&eg@(j@JGR1)0e%J(4 z^CJm0F5#FoY5C>Cq2_=~2qm1fp)K)oh|8gb(~uA#X(0THOK3`6YA6&=0uJR@AuW`G zG45iFv7KeH#j+OK)qDLz8tKhwB#lK9k@}BRSjphSOukql1fi`xgp70U!~*CKs`o@&>%q6Q|475=nV~GB)iU5 zFjm{48e^1`c3q`{7_Yzd1|Q9mZH0O;tkf_URCZ7W>cKECW2%)MhE)MShItW$DYc;4 zEDPHIdgvOo9ZI$34GyOAl&TuHYTIjsY#UU=Fz^2RjbUY<>;_;;?N!S#e9Aiv3lSY3 z)@DYt4Q3Aw)fl55H@w!VK`pYaPz%HC( zchj1QQc)tF@^UiBIq#b&t=X{Ohk3{Hu+TAD6$s)0I}B?fbbMHw!8`nQd{~R4l^7Ow zF0U=oN(?I%rFLMU6;@4kj8p|WQDU2+62t11dMbyh8Ety6V)^N?a!s7+GvdS!i4### zl!~JyikPf+=rksi;=S=P_K&2v@3u5sx4_tewtQHEu1l=6h$j48-gh4FX7;=#L?WL> zaX5-Wgal%G!NgGl7*LEMW)xu?m{-W8pWwiOZJhOqu_?FRM6{9*D{a777kpz&f`Op{ zx~3n8l4U4XBlICgA&_TO{(l1^R(W1(gd}3_!7VnYkAhS62trr z_R*6Re{yX%GY8H?=<}!pbxmvuE68m}*NFqD~`rp{I>xW!*@i;I4S4$hs zk6~f>EgSwf=)Av+@mnYMGi&yR2wz082=F(An?V#auH5mw+=j4;(S6tRgTL9&L;uv0 zhVx?>_!}@>A9nMbONmAzIBJLR?% z$N1eCwT7Pl2Z5J-``2XnrKS9a!}>>gUK}!A@%bwh)B7&Qa0Z41n5>}$3Wm#_Z*ozf zA$Q!WODLQxwzTTXm*)&F`L@z%Z9i1adK2y|?d1WbCegN!^OKc~RyqAOtxIwB%ZoUq z>rxDBfr0@XhNzqsOzw}vDnr%)dFJJs)7rx%n`vSlH}-jB7;_!FUK=i>Z&Rfoo0MmR zMrSHc|CVA}*CiPFV!q7aF^W}gm)V=&b$GA!9+wYw5zraGGq2Sl=*tuV8R=0_y!Rs#PZ_J+Mk!_$v983m`T7|Ha~J#G*u=O0%rREenN&*F>-(_k@kcjC zIrnSZIS{oa1X z^3@PaC?2{)lN|9s@8c`rE7$b#f!Wt0vO1So1~Ex68@X`R-?IJ1s#UKQO<yI1Z?cMc=QvLP%`(O5Q=)CiY$Oc5>g$GFkyujNd z%UJWf;~X4s3d3rN5*?Jf&RxFl=Lv>B_Qyoj^#OByj(m{J#0D-Nd^})(g2Jue@0oah`aiepZ^IUpJCp@{-Wy}Ed5ebeoUE4ro+Fy ztN%C*OACryurm{m^)hS+{WUHbe5yn{^*~mi1;452n{x%Q7ERopDY1O10Z_+~m~jGd z8|Cj$U8VQNhcFM5Ph+H~h*jW|F9cvS@9y7PeLGbltH(KituS(OHjhd1e9k>D>;aa! zt`9-l25}6eJsVWEd}U0VuDIm+ZVan162nNgc0<;cY5}8iGPkeUMRIf_S1%t8+|JaH z)#LeJfFYf4KR+~6=$B(aO$;Naa`VG+65UN4UFmv8Qj10K^mxJOQU<$)# z?*Ha~4%8(YEQf6U4{y@Iqd+#0urn{-%u2SO{mfagh)MwMdgPT?Rk>5!#V-Y4kjjU=kZ$_Z7gv5pB#nB%umV4t^)h9hC-}98!#v$kx#Pf zoT#gw5E$m|eEk`g7>J=cGQVGxUH9x8wsG*C-GS?`4F!GPOYH))qtHjc94EFvJDgbq zv7&k!B>jfOY8hsqR~peo9K(>ic5ySyF4sIpa!-jgMBP!)XXK3$3{T~ml^0`}wqt*}?12P6n>OHWc)E^R+aZR}1~5m}z#oF+Heg z=nV6AzWz}n(KtqW^9%y($;JH?8^C0C^YX)C_%E>adHMm(WM?n`s^LnU{Xc>EXg#9g7W?nL&IbkYdUg> zD2&9>WM7wL42W`P;|*k2K^f3!^hB_#Yezw!LuP;$e%rs_>CDp`g#w)1pXpao$I1<1oIIPa7v=-iHj!Or}6aah9mrYc-1Y z1yJ*u^Rci#=Pl?nYfcO!zmY#uk!g&3)-g6zdYR?O`ZFwja1zakJ4a{DXYAIq$CuN0 zWN+a5YePYwo`FFOA1}-kDKJg@F->;!UMf{oPlKe*Ap2nb8TQ^gY2Y=$nZFcAUE10= zvV>@2Zs7WBLqVTNY!QY9#qa1Gc6p*ic!dLRrHbllkPL4gw)M(1lM`FfXm=KzLXOTo znO;OB_6KwihT+E)ktm$8F$&Q&7&Yi-znzJ3e41n<*~!e~Z2jl=p$~iQVMry1F*53# zoAYKskOXiN$9^>ogJ~8Y1Iy2x4{`z!bu-I?od`zuGdi-JhksJ2r@=A|hGh*u@h*IS z?ihyq&`i1_LvMtbkI*E|#K8bf)C|Q1w+s>KT91~#LSH=Z!fbz=)!|+)I(nRUMho>c zSo+=gdNa)Z0BG||49Vi?3(x$F{TTC)Jo(+=ES@?vT|KLE>~g~uxl$MvAc-;mk-t79 z86<43eK@o+G5!eCx}U(XGP@)|+-o_s0g)uyT*|Ls{260=p&DOVEv|ej!N3tKFp+D3 zcy{fE1I&v;PF|9egBLi=FXlsI(bm4}p!t+pp}y=!dd9R**qnj$ycs z(H-~j;8(}}I$c-Uf5Zwz{S^>*G2HJ>f*mXw?q~1tzA7}Y3xwj8zh2bWh$;r>ZtJFJ z=t*EnwnanktCA$>OBsFn9)9@I@d`Cf4Us4uwGz&_eu%Cat595n zS8?|JQ9g1eCC@7*SN^49%`HR3d#(bx0wi8M=gRkqd}gL_Jc%(s;XCJsZme9dTwkyF zv8*wjI)7&$-E+=D1rtZuiq70l=HM{n zdq)@_nPlhFY4-1ciE&NukmAVYQF`aam@#{RSbQN-IUAABBJ{hNV=PA5o0n_cZOmVF~ZSMMMu1IgER@!;At`<`>!R4%#pxnBA| zcm@&KnEgBn5GE0`6UM1aA!vjI5m|u95h!ysu#gCIfoNeU&hY<__o*H(HP9>iefj{CyPuySY={mVyy3r~iVZ|G&_yz3C`uSg2-(E#6{ zeXw%1rGL-ETUL^3JHc#V?d%`bO>=*gHDsxIR@f-*b3S)tAKi1$2flzIQ5>~d|9fTW z2+u0BeU$x9uBfg%NWQhD7A5O}@T0_H_%rOV6>!Rr`#I)UzKDWrdh9Q#LA>N(9X5w z)?{e%8J9c^z)p}mczV-%?!0&sovEX`Q-|N@vdu7Baa0HzgU%brHkz?z?wGUhu*A9l ze#{b|^V~?H&3fK_>j7@Rs3n-?pwyah8{T1aO5tb)6f-RO`gUd>aSK}g1!8(Ysq9VG zp4_8kjiz&c$M?6X4Ef)rvNd$Q)&ayMG3GAF+{9%c`0qOES2si`JkhQJ#|J^4-KoHJ z`|~n2nr6<@|7F_rcR>b$LvwQ>cQ3?Kl~~8c9{RlJdX5V(0XK5+?dy2%XFK_yEzR=h zWf5c^)|@EO!K@{4+U6KD7o3C0@1bNVnBKwXUz(?GNb~$pHJNm!~3pTI=Q#!Dcm-~BB6Yg1uc4GKC4{U3(BQPNGdu%e6u_vI@{ADT zd&4mLrl+UJUw!q}nXaxb!5EvD5N@MI&+a=~{M^^q7d8xI9b@blob&(ToZsS{XSq%C zHjO)DZgI|!Gsb?IPN&!H-@iXxV&A)Mb26N7OO`BA6Ny9x&yO!+bquc zY11^HyK&>j{Vgpm|0e>_?c2AjlarH8jIm#F&WHa9jQbj6Y}m4_UrkL-H9hszQvnYT zbM?0sfC8W$9UW?4Utc9->^07LY%WGD#%0!GGLd6)Drd>5oFx+`lUa*Ni;P zkP{M;0wGG!7^aAZ6g;s&M}(guUOTl;9HhKFM)n(!ys~4V;>`Fe4oK6$uakR02fipUX60 zb)|-%Hk4s~RRGJv8Wd7ekc^aa|G9JLe)iH!FQs~WdrKsxoP69b6E@|873?N=`u_K5aAf1M-b<4LV=nf!Ll+HJEIlY6w#qm z|N0JPIOjhb8XDTWeED*t6bcFoah^C0TefT=ot>SXilY3S5K`%7-r^FMlLCKpG=;Z+ zZ(t}TkYN(Yk0F30_Oo_3Ujf)pA z{_gzw^PWxY-g`(10EkAT*t2KPB1&l|O4$6_ILFSDS^V}oLyo~_N)mgvbeNXzY6KAO zJ9nJ3xARN^D9+lB{TKHOD5agdckga^^2sNCyuw+{=L2J7V@)+RHE&Z&f8y1K#U;*6 za%}lc8e>_*X?rweEx^>?YLLTxmpJl+-gIV;9FmO^@<5ZD|1PM0OtLRvuV112( z8l6HyB5p~XoU-uU48y5}2~7dsX|KRD^*TaM2$B-W3W=U61B3uStkw~rg>dT#bGE0a z=drC@w;Jhm8t|IWjvYJj;DZl7Lm1y5LMlC)l>jApFrwhE9|&M; zO90gYf{;Ss1{ML}2Xz5Vn-YBq6G2MwL~Xzqd9p&{z<3rrFHZvm_^f>a9*+c}_#L}_ z{rdF}b#`{1c=z3R^9WD?^xCy+3l=R}^p>J1FL?#HFe&iLArs$@a+~=g+mD2Rtx**p zJ{CfmS~3*QNf|EjkCzhocr;_5BLvt~6TmN)RpWuM<{Qg7|M$_+(Os>rtrI}O2SjCM zWgQ`86(I!Pc6?Oe;)E=UF38*TU$$s?W0gKH=J(ym;-ld#W-OUs_d<%{#Ec0krLPe} zR#jD1)w%Os0cao)SW5_L^9~k5;*+x+lWEvsxs6j$5IkI?;9nmPVM*1!*EEN2<*@5Q z3Q1D}++CmGmLc%*NCt!uP)fahTQC@0TP%PegsdQhguU8Nni8kSB&4f(Ph_dmDE?}> zhVVQKQYIusiO3}dL2trDl1txshsPB@0bY3F1xg5613-7R&xOR1YZ7S#wq_*pq44M; zimh$)FQ?a42k>x}=B!H#dq^N<0^d&Mz@7Zc;Y}xmta?iueyqlOhLR!~YQ)Y{rwG&VM> zl+p%QN&v`lU?c&XaRTIX83h0b*48Ts-J|1rDXb7Y9MSBxT}}zHB_P1HjDRTsO)1Qw zlr}UsH>;sgh^eZoLRD22K)@p*gp|lyjsVor3>?i+i5k5S=jTHN5)sWon94$)DebJ! z5Vko>f1C(tnx;}pp(YXun#p9`3M`KRU>~%Qa5UpbLQ4DeB=Z!)R+uLRq0XN6NBg~F z)*hm!Y0^+AL@DPSmSqVcgy}9wh^^g#;>jL`Kj(>OCH(Q>-iS+J!VpC{74kk1pf)dV zZ~*`zgvl5aj4{w`HjC-$X~r0f1BHPB1!%5BKJMp{aH7Pi8xmPIe-ScF;=}~=L^u?b z#R-~p#mUEP${r@0ri}~UL=6;#(;z|Z~if9 z3Vb)k3sJ~r-r1)utfDwpFvcc^hlhn_S)f7)eEs#;!m_NhLI~y}Knbuh3Y4q<6WB(A z;N*3QQ#a<1z!yU%ddGSG1kR`A0ti!zjWt^Sw6(JX!7R%<+tt;@0LO6$2M1xA=J!%c z!+R1}1PLB)^f+@~Lx4trz&vk76Q;zcgC=HjMY)y)h$L85rJ_pnZz82MOw;^67KjnVC6ZS=N{@ae~B8*Amo*3Mddk5D8Me_ag;=)_U(D==>DN%im-$5SNhLgR+Bk zjYjcetB&FwEXx|3nVC6}%jF6J06;RCyk;22U~x9Ou9;wYT|qNdLEz;_2%cU>5mfF? zfJ9E>=O=Uc%~e*&Zl%91D_60hrjSazN;M2)FquqVbKmEaneOiHFWlwE2#k)7?mK?`_~zIdytscA$d_Q_emsK@r|7DRB$M|y#Sz;9>1kc#oLdBu&he) z?PZ!~>iYHTyI*|q#a%~_9?f5W`ds|XnKOvT<6Ws#>XO$F0L%Ex+0JOBU+|+gbMtyz# zGm4`43sMht4FIqG8ko$uZ(KV@;Reo)A@a`e6+*DNRmJYdb*!sX^MyQMn@%p5%bY%a z`tN#rdj7k+yW96u&MZ+s`Q#Jx^5x5i$HvAEGsetf&Ci-R>qgF#wcZ61K|-*pLB%^8 zf>>SaHyZ$8jG1F&V~1n0*x>^Q4w%K?eLmpX0$+alPI5H1obC1|Mz9&IIPuLq{Gz+{HNTUJ&U@D@FJ zO%R|W2t2infC(E3;S@az1p*47(YXttY&L6-jEwZgVzD>-`}+s(#5^zu0$zbW{q)ni z`|i8%*_A6-z8;B08gyOH&vOMTMyL3OxtP4)4Z|=KiNx6X^XK2YdGqFh=bwLm?9QUv zbXy;Q--<;hlQC*)YWfvLxt30+LxDhGNiY~B#Zju5*}1+;DUnDdL@XBja%5!WgTcYU z|2TN?;MBmtz+L6)ca4Bc(P$JqcI*hWwYB}AzP|o1nwy({R8dh;tLu8Xs;bn@qi&~h z&N+k-B9qCaQmNF$@bK{0H*enT>hJIG{^+BR%%P#7x&F0bP60}h@3*PCx;nh_$}8&9 zrAs6A_4TWCU0+pEQSs-xuD6wyl{M(Pu7*M(D2n3#IDt$igIq4hQmNFaVHiWnWb!`^ z!#FcCGIC~UXlVMw4?kpyMB)!({&oV~5rJ6?5F#8@)JP;!uIqXeA*7BFQU^eHh8a@I zaVh0^I-S0L_3G8ko;`ca$;nBS3JCKRpcIw-jTRnZHBH09g$wE0wQJGT)I@7*YpJH$ znTTasn3$LlSFc_br%s(hJRXN>nt!miOVR%VUVPg=M%d#~00000NkvXXu0mjfcx0me literal 0 HcmV?d00001 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 new file mode 100644 index 0000000000000000000000000000000000000000..c179bf053492ab2f17f09a22cbbc47088a36566f GIT binary patch literal 715 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 BRjdF2 literal 4087 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 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 new file mode 100644 index 0000000000000000000000000000000000000000..f8ced45f132b86c5080b55773415e84325b8130b GIT binary patch literal 9515 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 a0{;Sf*2F-`L=oly0000 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 literal 6636 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 qM%IAf*eA5}h(mYB5E4T(^hh^D zGjzkp|Khti>s{-;IcuG@&))ml=Xt_3)fLG}7)b8jyGO3{K|%Z8z55FPTSSC+f6O)& z(RUwW*AIr!d-o_h{8V8o zn?=9Ef9`ePzt9PP6Yakz^1>wat%4R)k9GXt?{WgJ*@0xg4}N`%Vvi90Ve%~GEz=_= zcMB^jIE)G|6f#3)$(WX7mzJ}Up6-vmUY7A`sdmsWX7$-SJ=%D644k#yt9J0yFE(-* zFDR&cM}DgcZ461G`+Rk=DF+?QMLWedEJ!xL&Y#* zd;*W1s74t2v5LMQ@THfw{pDEhE%FB+!XQD4fjXm$(x7^wfA0@)XDb-eYDC5YO7&~G zOB1+PS0_FqQQ6e07*YQH4i#bj;G}|=TS#$9hwS{$N>{-6$)vp~)}I2y5UOjgK0B}b zG3N~PgCew{Nc%et=BKxwUonB2q-rrrJ5c{+rU2u&e0a{$y_|!1#?&@2Bmpa=Kc){dB`Q& z%b;c&P>SoCKHv2^V>5iaoz0eXClV=(^^JD5c(wvLc{>XYR>t3B$??obE*?j? z_P+5E`D5a^Wf&!gtJsp=cw4vuD&bej;jAj_j%BNb;<_2S#tae#9#Ddo4G(key4w2p z;eMw_Mk1_0V0ftl+N8ciX`qp5V9digat)CImLhwk3UKzY|82NiEY8FLjZg)MJ%SVn zUcd_w$k8lVVg$DfiU8C8dOQp8Sovj8?C1QyamgGpN(IFXsQ3syMoawesQDv^W(0R( zy3waX&Dm^BnI2uDnZ<0}lr_oipvg1rW8=pUAs%++T zk7NbIqsOvNhnuLgl!2(!2JrCM8Pcz9K_!?AlJ}GJVaTSeE+)2=z}B8#1#Z9L*dOLY zok40TLTu;qPP;c2%gGB4$qKUfj34{WDvvxcl0NC@K6SNC!8Oc`7LLzP36K8qI%l$p zBx8{4iev8!Oy(IF9nvAKuaV;b7Pgwa3-Cfx94nq44a%(sO%W^8pXn~CmQP0x!+WQh zM2J;T*mBqfoE)O$1703Twn+!9=~VrwZuoJtYtQV>(nAAppE50^4S2n$1LTNr+XtfA z9y-$Fsa-~6+m~zI29WX6RiiM_ai+2<+21k9XC*mz(`fCD1U9RT#}TGpVDAZB5Jl5F z1B(?SD$1Gs?5EZBR-f!{2IDYsyulK|_chdB_xZ$$9sidr)D@CvY4vjz04*VCaR+^< z)=I0%J4WZOAF6|k`*HCU(8da`PraWE(CVQ}k|0NVu!gM6J`^qf&ZwE1-o3Fx)pF}* z!+XMJkzQ}MB}Hq^={@Ab_J%>982B{bn}hN9%nLV&haD@wZ?s(XefV)T?q#XUTJajz zp(Cyj9uZd8$blE) zd*A5HgQG{C9b6=a?jSR+b2v3n@T{OISGPJwbOd>v`_r7KpWBWL4VO6f;#xP^K6H+Q zE3V`#$!hG_6;sbUuAh8c{NOZ7MI^No3&V1WNaiGk3O0_OwA?P zfnq`y)YJ+;IYiv#q)ObKokzFhDtV|jkC*N@msNqijrg>mk}|3?hENgJCpUWvbLBy& zjtqZh1 zBZ4JEv>5|_*k1tux83V>I3?-dDhb=*9wjz1xb{`!xTR|FE%Mk(%7O@i4B+h?p2;1d zB}Z^%hZ?eHCxs{>Z9~@RWF$jYB*6y>W7r*B`cG;>vw5tPIJfcuV_oQ`&?6~@a&Ahb zXABv4W~}`*{zB%JhNY7>^6BtlE6TJ6{cyZu^n6+PO(~ zjg&J}$0mK;WU6Z}8Cdjw16oOAfNtKDJUA@c7`hynp&UxSM7G7UDDP7-#Q${#+b zVhA^^$cjpE*;MoHXIHtCL7a&pIm^q|EqN*etw9}6`GU}o2-moW?cPx`IY>GCY5Q?4mIaV$%I5YK-KXpz<$^%5%|a-u$XfB{{p$KzQkh`EGpu*lheMX!aB3rEDND3cc=<>)x{I zne*>n&IgV7r9DC;57s*tMHUv%?&hdUY|2#j%2+0o8iE&FX~Y0?@nW2gtW%}4f1+cl zS_s-35Z_ifThd-XSD(<{NM)99)C(OURZ(N-Rj5F$N*T0mnd4v%pCoqBdb^5A4v>5~ zmm)Dlr)aL6BtLM#WzhgdYY6jxSTvxByfA5`Pg+V?Yj8W`@Bkmna9I>lIBc#74bn4< z|A>2F^wImT*r~TLoXsmAC!vBj){$N>{G(gFx@Z)~#7M;7Ku~V2~=OjwcmWa4+{jG0N1CI@M5DA9BU!?|5s%&KHg`g>KD z!YV6=sDCEL96-PmRW%y$W&&10mV3^G6}UH(J#wSG=giYjeZ!137qB{s7@~P!o~S&# zkV>reO@bhoFrb^*zMv???YIjekXXew&697dwi(YhIPf@9t8v38pTf^Ti{ZpbhbK+8 zfGB;r>2RNDin1|k=o)EdE|#^58vQ}Esb zvOY%`#&T2eNtMn(8`~(bnEB)Gs%&Z$eBe|<##JPGrm-<9gEd}9D&Ds#Fb&hPB-3`X zLs9qVqc0ul4lBdF1cvUMAGeU8@9+b)*_*Mf9d%8$l8RS+1Hm+UO*00)TT^4-#uJ~f z<+$3@g2?ae^vF}dPDH0lB$kP>k`Uu9>&cS1Fq`e3r;(!4)DjsEGrTId!=yV44D8f~CRK|)sOz-D)Y~_)g=XAf5hwlmjCbbW(BCs)S zE+6&|>K~cf|EJBUG0-p+-s~ROJrK%-m@q3b#e9dqFGT-0`_lc>wWc44BhH@76wfVR znxVWN{WrsCaCXimvUpmlI_+SkTB0hgtx2~iEuJFxmu}Q8)m6g!khJxjli2FVq7hA4 zxFjHqe}pCD+3r}-QGRf&2wnY$znhm-z)0>sH$tu2ReA-D93}`@cC6LtAf9f21m)-~ z8^2zrgNOw{?@C+#zbm>X)woZ?V+P3Gx<3q$wrcF<4+&LdYh6{!l+h(NS$_~phR_f( z4zmH9)oKo5w6nj|D@VNj#5?=Lf&|{$`d(WAAr~NE7a&yy=^r#aW7ul8m++C$+CgU> z8!Ah_lnSzMs`p_>pw;1j^S5*tX3~VcbZUfK@_(O01ssXt@3B0B$2Lt;)dHT%vgb2A zr>@Gq%J`xSz1dXLYJ$@D?==wVzJRoX5;=7EPp4?&Q!#|DSGP(3I5YL6VY1~#TCDBs zHD!<2?MyrNxEXA`gRBHh`87v0x~i;LilwyEjQx&>fT{)|gxrW!5oUiF3UO0>K<&Z{ zKp_2Utg3cZA`7_`H%t;6_~=ziO`?@ZG%#snM#H|`hJ5`rk^5!*={`Xe#o#q+OsBp3 zIz@%DQLstj=2kNEY+Da})Rs}h%m>H;i!=cVhg8XRY)wTv5aiH@vUzxAP}rj7?lc&@ zBgS3C=5$v~+jJ?LZ_aift`!}mOXS@mr}`ERlWAR?zB?_hdioYN-MQ=@q;=nhjJ{1| zIo3wmtHGJrreLN$>6GKZ;1uDgN29}jC|L+PPveQrpZFQikU5J|DT(#;Y4K|GJstL| zdilr8VuvHq$c#5yxMR9%s)0*0Uq7WlzhNt3v;UwQh+ybX^#nU5c*i3lwo&s3_Fy1~ zPC<0mm2{Y4TfpGfkaWio25LP~$sI8{$-%%el~ zsnPHq7);b-XVEargg8fW?(G6iGQ{kNGsYgfjOp=I-z;0J62@Kcyh;c;0n?#l-wv8b zP5yN^TV1IF9?~RoI#~3N&$DrPO{!Nu&d8xVG_7oA z2mG{&1`}7@vCK1>?y#voK9MXbHU1`3RB9|ZpV_N7*kuPIsbTMU$$|KhGecproaK#( z&tbSWz8JNZysK=%uv!kwZKQM6^L02gw`J+RrZx9D!<`$=mq5y$`}T7tiM|Gizz;UI#@1qt zxAhvU!{?^T0Ek-ninvx-l2JgaobE(C<#5*80KG9hL)5Kdt^~WNewEWpmvn7{6y9y| zPaN?fhXiW#$h5HPAl%o&~M6c?Y^O8lFr#y&o6kBEG}aEr&^6(CgSNSfJ4ysg+T#4Q_rNTJpI!}M84Du*%#&JSSLW7oR^ZeDL_rK{3->-@wR{Z z(6GrwJ`NbSKWV9rD%7terc_)^c@gfNDvhN@_b=40@C;`j)!DO)yYTxYHwd^-D_1)l zpR#1NkDam^vBgw>*d>>2%lI{A+CW=No7?d;O}?4%;Q`CXNJnl}Q_P5#^TWz1jSX)y z1efT)t&#S_yCPN0h6I;Bo^@<-GSt&#zx;;dhR3QHK2D`j@P#wdzUqny+35+k%rw9$?GBx{P8AR7kZ2w z4xb|KI4ThcI6M8ooa1j*5EK%kUiildv*&0Y#pcpAV~BEq5Ms`202A2LiqDJw4=#y6 z2U4BL1ONNT>Uyv3;R}eu!aY}4Em}bfG#k<7_o63D6+5lzt=tP@uCcA3zk5-i(cvXp zwHDMLaD92&FFDAEusQ6e)$mtPy4}t8ZqQ#ZJJLV3m;XcUrMJB1=2-nun6m?6zmS{$ zU*SKKkiOIKJu}>I!8r`#P=LLokZ7N+6+~=8w*Bx#bopM{{i0FW@K<_aXH3JIOIhhd zKKR2RBDQ<`Vx%B=a;w(&+J^zeGbBtQ=TBsB>z92u*Oi)ajN-axxg*O3_@m=HtA*_< z8Ob_M;~^hkDYfrAuGCfgMs1DtR+L^mj~edUq#Y}rZy3C|Snaoo4#kfX2s|!%)$yvfYM1#KtQVzrW7?sKsr2%?_Wk=K!*k;>PFox^KI16nz6QKKK6 zAr?T^0)0#hU8OK(N+9_3e`y}(g%R--WcAy7&r9c|-?MbyrBXVOA$?gJboLDg@G8}V z0@9pGPs!QAMctPD^)U!VXE1;AuK`W+sV~drC{sr_H*4VhzY{p7AFoO&zp_y?x4x2poFY%W0(q?#m&8` zM-Lm{vWV}^eW_OYSBZN<&`b~8u6Z)!DpPsV%+sNZ9c~rfw2@K0KuxKbE;!xydcb=B z=H60dKW3&a^LkMUa^p9ncTO|2&ke{)f-2=j`P>{itv47I4cA?@iC3!#W&L$5*hdDu(5 zbnD~uOX;Z3*Od~ISGVU=jnTX1R(|!M&G>rp+&U7TeUV%#YguDLxCdh&_J*`>H?piA zPQ0}h&4nKzU?F9Ca3at?6ogyzwytp2#MYZE*RYX~G~|PA!mP(#96yiXdEn2f(g>pd zLlK|3w6g9aYFSsKq}Q&JGM(*Jn>Lb`wh6f`pKyyg5nskz|cB;P7Exb-T-)Tu?!yI%kg>j-QC~Lc_@zMV4 zuhAeFe~cVk(6aBSwa~1^9C3EpG+8owvRbN&Z0V=vc6s9f4KEDP(VyFJL z->e9Ft0HJ4-bf20{Mz+)=3cyQsAU8LVu4Pg@1%~_PdHL_N+!&C34|7 zY=C-OM}`*q!AHGM$1eUlU zt>W5^iSP^CzF;%*>#@f+z=ae%T&<)Qtjf;> zLHC0B2ijDk7 zX{W+?#wOWb{#aRW*Tp3Ej9xIXOU@-Ql9csWs{f_j%*0NPMJwsdd7~ z1o|^Ax;$EqzK)8b#hqXbdGx{Bs~noV4aMB>wpQibI!i0*&n*I(PvAUkh&A4pHA33b z?G^O4BdZXCJhcHModXgKePi`iJwfyMEccaQX_+ynWfN1&$wpvn9NP`!f)>HV*OJHM z((XS4z~%*m^B#*MSL(3By(U+^(Br088a%#w22E~u&mw)TxiGK4fPjQ1m}S9$?8KaJ zo_!`N!|K6jjgO*brUPue8(BtHTF?*%Om-?u1IM-w+YE zr)5I?OtTRFmeZM$cbWb>binO28z2r~_MS-MKiHEXUgDiP=eX~RKJ}t-=CMQdM{lRE zE?#=k2IuUQ?aU4z3s^su=~C^p`c7-zQsj!Nr$v;WSQ%&YK6-NN9`#J%-iaG_w&kzc zHT+O>P|@Ye9$iQ4vWp*E>Z#Y@K%lGeIopRTQe^)S3pDjr=}D5S`Sic}JFYe1+UFzb zKu|u*o_@3w)zZr^Ne?s(#w6s2CkzN0LIQW^9pRe55dGk#4JVh#Yt7^}@#K9f-=DuwN~9%=XKRMQHVHH;n=BaF!T5bRYAQ|4VZX zOrQS1-nVc-C*YNa@}LoFd{mHm^2LF>1?*XjLl|GPgV0@q!xUOPFxvBsCG7wm_mqx5 zO3axBir=U(k+D9%FHhhkdVLbEV!gi>M#w_gO&*v8*~p%O%obf%v7Y_mkJs= ztDs|L4U##e&54gM@!upcXPp|%L6gzcaF&j6DbiXsMv1vJvP`5RR}+&K1s_P8URDAq zh0>+Xwj=F{{ySQ=9JIoa8!%!KigA+s(bY&R^$Dnx-xvkyIF{e#XTOVJFau4Y$t0x5 zCe<*odeB-7U7~(s8cs3=h)OSy-x_bg3`cF2O&%2eoFoZ$Krv&ot9spiAk3uFSkHmV zSR7I;!}hGNYvN1j+d7t|QK`BKwl7y`hF<{<4420bjt&BvlIlFt!VV@qzf!0Kt|d5y zomj()w}^e5dd-g=Xd4e!D85%k^leFgJ-vtBA4r--nTmU+7we4bh#mV12nX3d)iBeK zp-7PlmmvUp{p){UIQ{><09xPDNRTR1gFEs7^NJP0d{LV(jb|~6)aP34^CO)JhzR=S zVe+p{K`D8=!I+G#Jb_3mncoVrp$v%cJiZh%gu~c>JGI#BiJVY>32zbOi3a^e&xKl| znXj26F;?&RE@Yq4BZXA*tXL8il-UH`H-MOzMeGM58 zDb{_B6ufn`ut{#{sHQDu6N5d9-h;&XrD5gK1tZF@H-DqYS%)!1K`;`SOy#8XWF1}% zelU^Pl_u3c`Ub2Z1qjgv^E^p zJzpwhr9e^Fp~<#TRCU}T&S&W)DFa)1C7@apf5^YKBtip5w;oU}%Q}ERMO0}$gof=vFcY;;^p=?eRK)?Mpxutc z_s-#5&V4W1Z3_qTHcwy-lm=-(7oXPAql4H^4o%LhsLA$e9lu5Yq>*%7S{)Fpu|CpFJyi8+xMhu zIoeQ5c*B*x7XvPk!V8--$DnqG`O7z^;CM&lQ#P-RWtSpzgOl81Y{+ToS8O@_6jOP7 z0QOxb%kfb$PuR#7KAc}N?NmCx^BRXr2_xV&wqB;*t#J(V;7Xp&$6T?9N5{*Z9j?q@ zbL9<;z2!$>VvtyTL&V|tlRed$DqvRMOY(PpmCOu%-Q^t5WE~*z?;{@xX`;vdvMF~e zUtG}d$kwiAP1jIQj#-BfPc%J*G561PfJ@#a{4^R>5B*jRg>Eyx1btvJ*j4bUh80sF zpA_{l8lel%jeQ!ns=@Z>q1wBEz$x2hlS?d%sVz$;1u%4N8TOy;quND;H8LJq-e%jw z`0FyZ=B57Sw_2^aI2HVc;1@up>hSCBpsRveE7LA1MfE4`f6_QMZN`d-@JdGMqH zmD2#KtB$UTlwz6tZO2psHYEp+!U7I`AcC!@ue}E_X}GqZXdM zI8_;ebz(vD#3-FA39F2iwrQpl$!t(Q(65!fxRm3&V_AHaPo^k}+fhan4F97Sko(Wi z@toyyhW=6z{szy!K>aaAFpkiC|D&Pccc1IFYTS+oL`jRVR3CAVj5gq9rI50XNBf6O z{KNr`w0Q}MhuZM+NMpj6rd=iYlDUPi%pEAhE*CR6bKB$$A&h8>zbf=i<@v}F?|eXV z-|3zdbB_Gl2$gU(BmA3PcAWN9L7r!u>az*=4aVD26*g259SbR66Q?MgtBH4Az{crQ z{M!+Tg`i2KDWr@X?C{6h@TI;c%P+R@c<5JAt`Po@q-`cE_zS08^+Y745O<_)~2I#Fjx$V_V7+|5ib@b!qF~ho4NE4GO z4QZqBkFUFm=N4Z34)&2CXcqaW0ynTm-@g&=1@-TWVT%+Co)0o#1f_+|^h~UA*Ig3e zGQ56hu+A=(EJN9$gWJK4a+Hg&OwK*1$HW{QRJGLyRmA0P3!X|3pDIW*~ zx^l#*5bfWKsXhtKm-+^ntHHnWIJH1ApFR418=ULc`7=0vxpel8*A!jO6cf+#U0i!@ zXtN`{aWE5#~v zRMtd^MGRQBTaE#XwVWbKMfi%lrs#GcbyAO=IS-}@ZX{ybJ77UV(wz~bzDF80uyug3 zZ`X6KvC*ve`f067h?}K$;Bu$LQ9P?|62J2xZ7c4*44MGR$p|kv#F(%=a({DR)6?<)3SQnP9tR8sfmAqrepd`?ZXzzeH(Qq$sU*avd+vay^2eEp3yBZ zgh0c0Fv9P7N#*C3>;9!|A|P>FH5KIpAL0kO(^$#yOe#O}%qC#YLe|!37V{|m=jQGLc%2|vt zY*&A)6wk9StK8MpcNrSc$hs6w4HL){Q^EvBcM)F^LLvRt83UXgW?}t^-Q&vGi*s3x z;&v2J62SzKCXf^?9}6rXmWEnOMyvN3D1(Dq+dZ@O=jx*m1yP7rN|1Y-Wa4=PC({JK z2N_}so8{cM2mf`1(KEWa*P*sx^PrRs&mtReMRa@8r`#+Rz~f@^MrM0!A$LxExkSj4 zcF{fkup)Qh{48_4N_RQzDATX&SoCK6kn;1*ML6+(Fjb=2#PH-$$NVFw`+|!da?LR= z*9BuPwGsO@%61<}csfM^a|Ux!p~)g4P!E|2qA0e$|BM#PxJq9+Jm=ovEkFd@o}Sy@ ze11=cm4}hDL7W&l7ZVK8eHYrn5K~e`{3#j7KZI3KXRUk-=@~#4de&~@-NT7#2x!Rk z(2?>|ZkJz=w&sR}IeHvkp1Oomm43T(4lWM`L}Z{COD=z+rSLClXE~1(x<4fNuBwN1 zHi}9ytwoS04og`lp5{5Id`W@s?E5hH!<`32dn^++JddH6#v-kvT&n&NhxzE(cfUZ@_mNkoKWpgTn_hA=X*BO?_h>%4 zc1mS`Wra6J4uClz-RZ$RT0(ZoMQ$o*!YJC_^;zTiQ3n#s zW2vfX!;Lpv^VWlE-hu*x!rB-0;&q582dFPJHNlozJ-^22T+$Hk?F_;|in_#3mCmY4 zGse8ANE%)R|8$L8!g60lEq^utzLv-*NUqn6&uApyFJ$y%uBauN4zE!?GJeeVH2O~@ zKYr}>vq93WnN|>!&BN~3c$P`aSr17P!22Yr3k_k_Z zsn2oPp3<-+%C_$`&ye~`v9^@$Tl=uog2f*(ikn4e-q_f)Q_Rk)Y_5q5bL;nnq^33M zF_UT>OpC~_!twKR^-0yoTi=1qZOyAWwtN4aKxi5LkQnprBeYYbAxql01d2M2+zlvIb^3&Y2!HfUlu#T zmuKds$K&Qx(k2We{1z}dyvWUB94K;ek|@8}GdD4tS`e5_1(7Gycs!BFbtAubA!IaTl!<8B@(sqNvv-4g%zS^KA3llQVvs zh=~B2HCmv3Y!)aMnAF#6Cpb--N#&Co#^3+`xm7Jl5&**leb)W=tsFCRCnkCDUOMQp zuRor(DT87TkL)QdQNZ>gs!0*w`WY3T(4~Fj{Em9uVg{AdQ7}&71bXpQB>C zE5o);H(|!Z(DJW6wyLUI|F_oy|D~ z&d9G=GhMDkq83O)=-O=tRq3hKzS^$l<;GTS5#J684xAm7HZ#DuJ93}4@%g=;P}0q7 zS;`cP@Kd}MS@Y$^+{PTw#XVAE{=EsjHAsD+I8`tL%Cv}musJH-6PG;}W>qkl%w#Um zf#D~_T=GjYSC%y#PYBjUbT5C2d{-{?gN+WY2_tX98wU3@&8wjefaQB(eCz5KF!Ic+ z?#uJ?Ql6f6c$)f#ovpJ80K#uWBKK_d6R=OnII7KJ0C-}?@^OE9Qo}TzGaKlpTX=ky z>ccGSVrE?W;r{fYtzbvj=bI@GWfnE=cFW@MbPE0m8k-afKMlcP+n(Q8M+%L@{-akkj zp!9$JkjgsejHxZFKG$<@-dE5v8>EN?w1Q)rh~_u zHea{MuecFWK3qpZ*BGQ55Ejvjp%_U0AaBikcl8L?S3y`P&Wv@f`u2Ao?`e!u^t~JM zm_Qq91JcmjwM$rj=RZqO&LFoO}F z24V2)ltLWR{5`$tK^Zx1OM=_R2;)D*$^`B^?&xsqLVrh!CSfNI-tlYO((zTx!!`%l z;eg!Jdj=3Er^K^s&hp8|l0bZ&a^DNAD4H23IY8O`q_ zEA-P3nK!US*yhL1m-SR&H|2BXCVhUdp23=h>yV^@IRIpq+k6p9_n(KOOW ziGsMnbvnJnOsUVDjv|z$tLrvPL2V+fc`Jv@(g*f3A&+B-JmzfF9|0HGpv24tGWj=_a zYS$Y{bLq8y!{0tXQK|pF8szp+H$+^#qBHbRF1mYOul6FAy$`BP-%EW}MbVgy6tO>7 zc2p#p7sWUItQSKI!eu3Hkef)8-Pj>wE5y-4=`+^Nuc)J?B+;P z_~x0y@l@>03=?TSG(rinHrHSN9k=KvuEDC>>nJz6DtT>CW6qZ&pt4AwkW2WqMn=JK zc*>Apqu`ZQA~~+eyQi%T1yn%jWLFx5ojQPi&%Jx!LVO+lz-uj)0x&-As*a zU|X5))9E(qK8j47g6zG0iStF3A~UDL6d>&?@I;$W1p`2|YoZu1RvYvIOM$HzKB=cyOF^B8<+e_aPwq2(?5_6ulI{G(6{HmnrjD!Av8!{l+ zH@HDmCY%gykn{TP(!JyLO@y|SWu88PlcjN;44YAcfO)aU32l`yd#p=X1g(EmEs+L8 zlTLU1A(66-DDR(>DfQr~{6~6?22h<431jL17AkhxNP54AwLcp@vu?w^^4Of~z3TTl zt2FDoCi3C+TWbz-i)o|cA@6j+qs`|ol5!1vou(G8v&3xLE=4Iv*PTDjae<{a5_Y~# z#5rb1(7STnB`Z{!o|zO$Z@^22|HgF~L~v~o4CA5a;|RB6y22>W`s(xw=VECD2_@e( z--fPNJK`9mAYew5cRFN5%~mM}D=w#5Je(oQERO3{zdpG!O_w}>;=i}*kLl79(9SE~ z+K*RY;z+Z}*N>eKGP|C|PsXuY4YD4qhvSCi{AyBT-t(V?r&UOv)Q#HVNRpXX1~SW6 z`SW{j6Yax z<4C_oabZa_e6mW_iXYmiD}9{V+KEirC#wdroA*ALe$+~B$0nJ7uN11gl^?3SCCt$yg~oF3URN-{d<_yB0M0_&d|%ZC~fRA(`jw zUceTeGrm#2C0CpMdB_4)k_j96QgYSs*jD#)rne~K55)d0+5QRYZc-parN&26F}fv4 z*{jJdwTs;1%5Ck4Qb&p_&|;)7Zgr3pi}rXL3ouXds~`)1C zzQB!F?t;ID;2;bvyg|&OlTEWzBt9zpA>Lw;!56knQ1iAWvza-h`M}LPfbhoj3XGF) zSBom(8?$aFjNt5b%V8kLMe+~yV4Uy{Lgvcyw(QZB;6ld%(t*o&SB-e%T>;+k5Vm;e z7HG=l#tq%~1WA%B8Atw=Gp6H4rFg~lrr237!5yTthT?G>`!GqOp2DV)^xDRbi0zjZ z*OPC~DHz-}65pN8NUG6heeU`t1?<>mH}=l<62Vc(;%)dH>{wj5+ZEkWBwe4VE3q4Woz1jz}RFfo`((ebmImDk2mjw;HdDV@fyt=)PNxRJUnHAIb#aE$+NAGdNX7N2vF z#WDw>n*TN;Kv@6Us++H*DX#m3X7U$>h6-2Me(Arbs4Mg0a)({;vX02G{ecd-jlivm zeY}eJj*h}dr>i~y^kU3zfheeWw-Fin7uvQi-kFuz*BwCP$VtN7O?>FaoFz_9agyX>8JH?5de}eOso~d-Z z1PxMN&``B{Rd~0s9vwrj*TM+xuI0X^4Jae2GPhmUaL#JyYgi&CcDSR8LPD8ugKBS( zL{6r3%)Y&iQ8bSO7R&|(-YcqS9gbUsc7=Z##2LNhYkq4;*Pj?LL7q7+K2>4a0pr@c z>6yxWmQckQw+XJ27$eFhKg`{mA-Dg3=Fqcllw=7w>7--0Mq z^oUNZ>x3jaM9(f+8G)4Fi~Zxs^X_Lm*rWaWtKn@m@o_0}wlti5-M=rAC?$(kRwa8f zT~CmO+13-XaTyBWj*2~CgIUHM1Dvqt1?0R#6 zgF=xwLxvf_3{28y%Stbk(h(rmhe|3r#cRJWiV^hq&msee#Pfkl`%s|7lA;)p!i0AZ zeiiD$-}GpQvpd}aT-1hpy(zvE1dA=ktopm=*(@RDhq z3D-5Qvf@Qmwg07}vh;p;rp2`{mmlPDqplSu63s=fGvuD=g+D{Fp6G8wE%r{%z+z^PCZy@?&~#EQ*lj-?k|MKF?s55m zWiaNRiF2BSwm?b0!sl%U>(}&j-dNV_jI#)zJ9)+S zl-bi^%B!~4LC#a0PKU$rB#rHJ`%L~oO^HWT5+Z3YOaD#P^SsntJu-5Y>`odlM@!7e zzW&ZH4o5;%kHqfZ$rb5CnQa`&+iNF4jB^MlIQI8aE`2TUy>O?*Gb`0w37$80r8|4B zZmAr#<>Fo#JMAS}>0#@)xElt^Z}G@z19c7hb*ma3=E`fFqoJ5Jc%>eGDPCLdd0+az zv8W}MSYLa_(DTl$y4JI?;jx7|ly`6UG#UDzDu6DJ^coajC-qzN?7&g-yCx$#dqW+~nhJlAMh_d`2i@sC-6THact*zkeBlC)#c5?5amaMzduv5}_9hPIIuss^&A?7NUf|e#pOR-Xu zw)WW=S3HduZt$n_Xfvap$3G*6(=B&HIyBawJVE~zhEjhoqBM-nk56?! z*St8`S-v$O`!JNed67z;l;SjXbGc>7nvEI*Ils79HOPylC+hl!_=6J*P$<`ckjuS>QAcAbF0FXA&y69!yAC%eJibZq`lI) zzFr6<@;>sDCQaSuBB;gEDQ~XZ%Qa$r0B1JWRE05PnyNd+#^^npY95w^%q4u@rn{iX zI)n}LaHM^0#tEN0`9iTd)_nGPacVs$c0%Tw@hRa;IX5-!r5utP8e+4G=ApNvmV~=Q zjhl3pR}nU?B1}wE2P{H3o})>LsqU(URwrc4Al(CkX%wK*Jy=EwMN~`F@6F#-SgTy3&DEFOoz-m9@@SR zdchdwA?|~@^}8A+i$NQS8h-D2Ex$K;tuZQWq!DWC@Yz3oZbD`&ci8m9%GKu9qaFi^?}fc}I_zWVwrb{?EK7yu?#7u5C8kW$ zmtxfLq?DCQ7+X}M{3TutcM!!__q*Yap4iO-(5K9XQPo~W|7T1O5ccfnnwLrWq@1Z_ zwY4$k_;Wd#G5o8L(TYxLWYqW&!Uk|9aoa;oQbG7^9mH7W?eiwR66X+3V;i5^^=vs0 z*4+25Yn_&%@_EKThw0UuONOSPbjO&2EU!dTt??QYYBhYr3}y%?fD^sMx|K@Op|JIh zH)c4cy#}N)1i}Fb7xBa&&2+&fYd2m+RN!GY`p-^U`eU8K&Uq->+tk-OWwKn5XTo0K z%b_j=7AfMdE@pTW1~|WXD)mWVNT=}`C1*r)(c*9&=J70EX?Qh{H;+?& zZt-%;o04)N@X+uz_q+x-WmHqU-zngvc^Cra`J?V3$+A*gN4pB05c?wFa{lpfO&&2v zW9Ni5uS$0H98EQEtZ}u%H(ht}cNX^&xnyzD4E9^|^7Sa=9aK4ldd~DVSQVBp=hY^Q zIasO-tIstwoeur>Q{KEMi-qm2T@z1xh|OQiW2?#69Nisy~xPy<~O`dO4t z$;{(LL=L*4Zs)oeCeZODb-nG!JsdlHK4H9=y$T&p5y@GYR(Y;&X`QP6Ht((JN}ijW z6Q+bGSEirqd`4}KLX+MxQv79MbPT55DPsp2}Rv0$pmv5U9!d1Yf z?FJ=>t|GU8T_sS{>d;j;qh|3KBn#CS2?Nt2R& zDCNea6eAbzjG>_h!NQSvi~IT38t@q4qFx9a+W%z1lE*wzR#iIu4R*l%=5096GGv~r z-%N;S>u~qs^^9R@tUiRsE<`J7Yk7M!hbJYE4#PdK!p4hn6F5fyJL&pfot*Wq=ZEkt z46$KcfUur2gFQ(ow&(;Q9MjKzSPUNv|{?PnX68+SMyhN99S(S?>J$HCEGb*wU-cLW;Y#3YKpC{C{x;X@6)Szfmip?jPBak(PK3xw2!BbK zoiO@rIR}=0WE7Z+o-Dj!I_y#@o?gqIj20dV-uAgP7_JczV}KZ*LI$E!&q!1XgurWk=02{FMKv% zyE#teF!4;2uCX>k-H(#BnAqx9Jd!jg9VH z@J^W}R#f*3x*a&ew_cKqqGXvj=PgGhHzxHuTt3q{IXo?+@2u=HN{gDF~w%^W;_t z&37RV(OpB9$8PC0hoPx7nkXS_5yGR1Jd(&-@1(z>o~I}e)|zr7{FOg!-%o`I zKAL)`i2OP8-U)MQ2&}z9%dmL5Esfs12(#2SXc_gj#!tzZjXHDgr1si8C3)Um%8f21 z>A{wU1SuJ%93EbYOx?P_`=mL94*+*|y*7CoEa8n1*gG_V?L{@~F0iI<-@{lV@+cyY z<%=I#)2|$r;!Nd{%64mwVoKIpPin?>zdGZ78wVi#D`L2``&fm=ozEaUh^8T(b+OC? zG2CrO)c^NId8W~6TxyBfjbiBkntXWh!7uWOXthyz# zrOHzt^x{5-TY+2pVR7tE{@&@cB=LHNVqG%MhMps!=nkt9PWhA;&7xL7Uc~#@1W{uDE@YA)$>IYcW~nF!;b7F zhM>k+SX9EFN|lv{*gHm6?nVkyVjy%VTH0Te+Ot(70wt%DK~8o>Zuxt5bt#-LDDW z?)6X4Ao5-ouR<7}olKM)o}dn&8|rwIP3Rryyu95Q+iv*0Net4cLtbyX@47Bdd1~x( z{3Vzck8aDuDUr+X%d0=O55l)YxCU5`#k{%JZv_}{5G?b%ePh`PpM_VbH_z^zI6~z2 zd3n%%C|M&p9p=`JhnJcUHF)~j)OZ-zGw?&^+-^UX#)dmJ*i++KyUg-B#*Y*EaOY!r z@^_h=)ug=$D}&57%lZ=VoiHoNOhs z*WfJ@-Gu+yeuH+$c&YH&e20*N#U$H9ERrb8?||^9{O-5w&sR^qVN6M@-iYhzS3bR; zz;^&w0#5E%d--t+1{r}Ac#xS$Sp zjwi=wE$WiCa=0lxFaO!S{>ZP+IkR<`$gN$YB^E{}e5Py5K;;5i@>pSZFO&)ZA0f_v zolo9)41YYe{Hgv~{3)u90Gct^lQ)zv^l;Q7QLer8nW2*tJh z!7uN?q$PFw9Q8b~!t2gBe~1}3^M^l|a~g7aGhNo)YaM!+)^%>)c=^iIwZ*S<WP92JyXw?XLq1($llTXCu+xd$&!H=ILS_ zCB|2C_yZs0&EHa+&ak}dW#7`*9$twj=he4JFVK%Bg zhn)?~cy-Fd>rq>G3&Okk?5B_LC%vN!xiGFoo0ET586 z?tHPp23|u$yon*@`FcnG`V?W$yNKeQL^tWKa!%GKQ{-Z@LhOatIR|bghL>{qL+{{M zzM=o!jKir`w_e>6ywu!FErjN|)m91Kg6BD}hc_HM1mTs$a4pTZ%FDxz&X~$^irsON z*K$+r&50&@(1$4vrYM73fE)R)uRP5yw>16J{mrf48XeY*iMRQAUg(g`awuM^4>Nrs z|K@8uROZM7<$Gd^}LqGj%zNAuffap7q0lH@U`E12;y}_t|c(@J+_AQ2Bw(X;)waq zsNUFm6`N=xvd9hdN1WNb-4Vu0gG{Z%De=5i%@E8<@JezD(2dti8!ZVvb-p5%Y_;U^ zCb@j(CQuF8Q#lSj8PWzLuu0_iXb)HPtQYyjwmj1FAfmBidP#bY11|&KLTEqepO>+W zQsYpIQ-e43S?YP(%lV=A9$?S;&w=nfV7-&u^+I2Hn4%9t^hY>*hbKaU=C^ZFb%}=| zyp8i_`Uj<&KGclQP#vcDQyc2kcta?MH@A`+GM6;fZeUR^{IPhwT!wbrbGWtq!{`Nf z9$@dGR}kX&!1hQ^o|lOj(~x%|@Z zr>ma`FMP`ZX1o}90c`*9yd0nAzB4ZBS?4%BAAGc= zTlY~trNUUvS2N*Dta%Nd<;i-BTRyjT%~TFEx6773U+{eJH78*HzgYYTkePG0SA>^u zl6j^8a4UqLW%F~tYsgZYBh68&-YRm=HB^%#>CyxLDSYp*9|Ha@k?$dLkr^UOPf>b> zh;sYJz-f$O=GCk2XPA#b_&v_w-@JP`ywAgV&*m7Hwm+)RDK9DKVWh}fAXB-}sSsOV zdxqQ#lT-bjQ@$}Qcs}^*IWgWu3?BwWl3c8SnEI<)rY`gIVl0}ggLqHy6n8)tqDQ-;r>JC+S*`*X9# zyQU3J5Z8ncv3dF?e&QLaC`nUIj(xZe%MDdYn4aQWd7tGsp;(F@6Ea)5xS=E$Tq?(@ zuPvULx|Z_kx}}(!e@XxNw}-jk<*x@G2W<3S!I2?w^E=HlcZIwU&GoIs@G6LJ;y>O` z9-vs8g(qL`c?q6{nL8}=#>(61nli5)9Vu9wSeNe?kI$I10r7I+2H;-+8~PlBAPk{A z??RM8e|$7GhE4KF53wWo4m*AE5*^|1&DzLSo8y3dy0 zQe$rEJ5^to&DC}DOv|BVkQ37yr>4A_m!5o*N6N9lf9+@X5yEpJ`~ZPRLs&#fK@6K5 zlOR;n8!Myr&b7u2iYJP4g}=PKoxtn(l^^^F?|PlpXYJ{v)^AhZ@TPRA@w^RB@_Hl< zxivTBUyYaQbAuODnT3bob9HDu zH8rgvY+uK?1$Z^H@SpjqwUTeJ(cbVBIxEE+GTyaOge99#uRF0pjE?}mi@>)L;^Mcf zd&aD>#m-58%VU){*M_Ut+C%5`+1!@nDI#wmgje#MgGaGb9yyv$hj~0b?j@L0N>X}q;eI082I^5AK`+Fev7c@k-)>2 zYf3GU-dYMkycKvAn9{F(FHBy%ISkk3A>e8fSd!)QmG{4l$2U zWAUvu4+GYh`Pxq0yZozt|K_l(v0RPO)BXo6bHNf%$1fe=TEE(pEj5;T+(dRdqo&Q2YyaB?x>xBT~*>Q+3;$%20P?Wdpj@&4o8GfsSVojvDo z5aR~3^;4WWeu}+cw6*=kq+{Bwp;8^w^8HeTyH4&S#;*as7vi@NH~{$GoaHT=?-9Jm?ySw8o7Z{+otNzc7TclDF*hyGzN z7aX{fz_k#+61aF<*5eRwCeHsGTcb-d)d!gE zc@(qoOyC*gC=J(P*%~OB zV>vuao)Q^Xh8IKy9d=6cTzDlR85jSPO9@;<;4u&%M+grFW~S%u9SgOeAc+YcwqK)B#!`pb zlD{2e@p?_e-If)W`#FC*WV@B1I{z^i9muzw$6&qE0DNx)+X;p&AE59@6WX}sF9?Z2&O7?$N> z`$3OK)D4{*(T-@N$1mPB?a0-62k{idcS8Jt>UYh0E1BaZcqMkB1g|Mu6&_5~=aP_F z(imeR+tBTd#no+0g|&M6(rpui5RWqprwN?m^Pk$})<4_g%mrKg!4JZ*TgqhnKfZg$ z=)vnm_7UR&A_thoO9@;Fd=tdSL0DUQY9dEx$}0alC&IGNozrLQnQEi&@hcqJ zwho`?Zzb^K%+B1xb1v@7wsYh~i@ObTQql%7wQE~6mFHDTwQX4ZV`M%X#&-2>{>bJb zX7M@#&ts7x8hunXSF-Id2~!9s({U@K1otAs!@fF^ex@NW8+7IWUps$-0?WWvuYI zp=RXT(Abix)$N_Y^O?>6kmnxSWZ1Y*?aZvoIhv9tlhF&AOF?Tx-m#Q3WF zY~QbKoPcT)6ct`x&uh~!8mrac;#fI0r>^uIHKyqq%4;u2 zXJB;7Mc+DMw+L~q{*>~#@^UBe{eY@yg=8?ukhsKT}^^FVm39k`)mxRVW9;0s0czg1h;q#ru_!`b^{XF0IfaW)r zQ|qU8{D*AfHEXz}?ieSl1W%t0jMCvu{GE{pl8xt*t6u$@wsmJwyt!np73%8pl!4ZA zl!VR{Jg*6jr3@uxV?W^+XB>LSmCVk&9(XFTIZ$gt$!$In33fwZVKKdLitb#?aM!{? zJ(r=edhX(>YgBprPlw*JUK94lZasZ15%`U5W0)h)<4KjJ zXw3Ze_){`YjW+~0tHU*;(5h^eTgI*Qz7eQ2PiXPx>ZbTT*0B!dP+WThtrrt zcKH4#a(#q&<`Up~h?cAm_5c7N07*naR8L!+*fyu8V#NwaYT>X2aW0|4=f<#zh_k(r zA-2SB>x)n_Du(ux9HIL>DB&beNB*#Eww0q5Pzthr+jzRxbE}@D(C2A&D#~S_7lwBZ z;Vr=HITP*_fTiPXg^YUKYv!O;J{C{oSa>CJOof-iZ*?eNyRpE==FK$L7#wdWNEtfM zkfw5vIm4Oa$%Wn?<&6DsyeIwI8f;xn2-g$ivw-e8Zr5!h^WX9|-eg=ldmY0RmPSc> zg&kwoG^4{<8MDXE+8^3U#FCvYqzw&x~)_yBTZL5>nb8I1)jXLVlqsYA+QQ*JK>`!53)>zq85+D z?S!C47~8aomPd#5DunOUHOLC(+jeBgtghwHz4k~L$R<42jdf&>0H+t9{f@69tXaRD zXBi-G-gwT-lz1ii`qFt_!k5Ead7pDHW_xa_MLEw(Q|)e(N~Y%A`BYmLW~waX+iBc-yj&0QUs)fHiqoC6wYXnHV)x;z@etW9rH> zNb9$*ogdzsafYjj+(6*T?E=-IvHabS&S#b-&-SHk#d~!|*0s`Y?cWAFD*j+#8<^VQ zxI7qgM0MTNQ+@8q9s6aSd)4R8SBJ3$@g}x5zl-Z0a4Yo&r74%>MLH=w&6`{MnlER3 za!yGKZ&iJ+!pr3htPXg3WsN-E)R!fH74t^&GX@&Vl_}mH84LRD=Wo-;~_?*L( zvUM0w5&0t`uTaKtCjJdck$48qiqukTM)PW(+~%LE-;fo_<-+ll0biL8*XUbSOYqWg zwAQt?>S#erjX{ZwHHM{xS5K-GhIidi=}zn6FDZs65aM%)VLw27fY6*Qcf6{m4$Jc9 zq*RBb1JdTqVWK8r>g2%Q6=&S8%99A*2mJp_-G zsWr4)gzygFwGci`SsFxAZey&)(->3ZX_^9Wtc+E7d&G4!;>{wr_C#r@bk-(y4bBoV zc=Y{T{B7ir_mW4;X5nbc)lJDfCRDdDn}2iCGTZdPZ-DRn#C~FUGa)>vi!K&^kYzR3 zO4`4rdtKhUwn)hh>OP1vr%q3Y8Z#Uxgtsu?`uBY1w!h)u zKj;k0HKgZnC(*b~*4XPZi6_?v^w(c09^05h@HAO?sXkkj>hu5K-t~oAdR_J3KDiF# zI8Me8MN5b!2GI&J2<9PzRP@1rD1tsHK307(303o8t(YoB1gj6C)(1h6f<@354WSgQ z7;7o9EgG<Sm9n;EYLT?x%hsZ2lioRDx49~+GIcDD9J!Tr* zq%#FiS=JayPlT*??W>26RQO;-ib-Qo=+X;Yo;QVXd&S?VHKi;Aph|zw2XjnEY zN9k;ko}jkSQlt>5^-F?$6zGdrHqNr7f9gPVky#xq5d3DQh#&6erB zUsTH4`G2mg?3xU5C2DGNYuPg?@u%q)fKLGUW0b|m@Z`1n(+~VyUc0x}LjmnGrU~nVWQRO!%Zne_Xyuv!rLe>?U(SV&D2&1Hgde^m_0m>u{9rBB{QeY`GrQKOMob~l*K)A8b-9#j{16? z?-L&YUi+t)arVRZcc^AFgl|({H-g4O>hud+5?y%2US3!|m3n1QL|z{ZEkEgqhot{I?Z$^tvh zc4_u{_Ob-X`QRu8jkAE;I)id!i%oYS$$X^sz?tK zBGEMxq$ou&rGr|oL&M>1{5_Y;?r{8&*0V?BwRCI!nqM~gC%gjSgP3pw1wRSklKkL= zyZ`+Q8_C+%0?eIkYn5o#>#|1&I1yN3UL16ZLY|Wl?KUAs#fn= zkgod9v)xkw&jR>UO!y@5l{fL!{nCA*i!pj1shpN%NOQwVN-R8H6-uPxRhbcE9*x)K z!vc9x&zUBBj>*vIv|aPTDBoB&H>xMD^NAZ*aB}560Dc+;?*edjqx1zfldskH`9Ut9 zSJwuQTAEIqvg&UmBD&MwVRs;uPC!QM)_gz zNO&H<)!)KD$C!8G!$KH)7GP^&$_4;Sr?pc?vscR7i|65U9WCSqSl~UcUc%Qt@F;+v zK$(6FC=a3F(&qUdy;jcLXg}=}gB4^R^_)-8y}@Ih1!_Utu*t>5DS$6xfoCz{udrNv z8c$!Tfu1-%C0(++7o;&?s5 zuqh*_dWaq%E^l;n1Jb>s6X(o zB1qYmk&(F%N!aTo$gX9tQZ`o~dQ{!4?q6k-OqODD9%}1)K)eOuDt8{H-oUJG0TZqO(?h`Y6o4PV*^kqoJ&jhgH5R-85>O^= z1|TC_8}ij*3cr~7Njxk#p6gkn;0C7Yb13)#O8FdK_{dxM%U?J}ZoXJqOi!t3@o8-c z+Q{2`@ilED7_dLQZ1k-KS#0t}N@L9~_Xq9Cm{z&Pv_vUypiIvL(?8(t z+t1;DUwIk7``y6i#tw`*<`|7vuSZ}~>=Aee<3l=?GzC42JY+FuEH{ zvKh+m$US&xn2-M40*}4_Dy}~82nv1#!1vG z7QmEJcY}x9%I)8fT>^Lw1)l-%cUa&#yuEw_&wuzf{`Tq3F&yc_6W^n~a**x2HtmBa2e6r?0aOIaSUK28abk{7&tN)<2b)a=btzMOyW=>qrQI#3<} zrpJNuJpdj@nI1Tkofd1~q+Ln-0eG!XD}}{inUIs)Pf>6az{|k&0t)^S6JA6quj0kO zehVM@+4}Ex(mL=;Gilyr=34}h%R*@L&i5=lYs?;=#DB0pv`S{ln2|K%Y*5QWt#&9p z8g2wLVp~*?CY#w|BV*O^zV2b*TYh|jcmK`_P_Cok0Zh|l0KOjuj{tZO1()=v&k!;` z+Q+U-HHYxi@|nv}o{clbRr!1iWqKXJ=K=gPP(B4ruK~-~apQk(;6FaT#3Mhy#4{`9 z*5>B5)plu4WMnlwPFl6(wQ&s1Mk}C5ZP_vJtq%)h_M)%1$gM@44S>|xto#Vd2DDvZ zj=+lrt}?WPoJ%TTbm1(0B!=)n*hED zOfR97*8uz{3OpOlgLIC$#lGpO~SMSK_pmY@iaOZA8IAD z6(mPzNfAB0G$-CD?8wC0CFc3Pe&5?CI9*=F68B?)Zv)D=qTrhWyyNW55!V6S2jD8o zbQLI<0Gyy;VScFF`^v2{odUQ8;4J`epiEx^@H$HQ0;cID6uf+v1uot~`Knv^_0=zA zwA>>r!^-%TW-eCnisDi7Y}|J|-loykd>DPo+@E2@jq?I)OI8L!z87WgFo z8QR2q*KYtn`u-D?atQ^OaP}RHmoZIOQKsv_bPd2aqLgn3a6f=+DAP3nR{>l`nJ%N? zGA3L?DJMb}c!~wy#Dq6c@CM5CDyH%U0RMFs45u%n;4Lf`H?dsa#)R8==BxQ4q!gXU zbi5Xg#n@?rq~md7>262gK&Y zXuK$8=IpMHR}i%ELi%t){_6WB>kEUi_PHpsu&sXjNAZ@7FN4|BFHvOI%uFVacQoGU zjY8U_B_8X&M45XN8FRJ_^l0=t%Hn0lJQ{B&K3uSCO<0@YQbhK!B9psfN#_O(e z(#f)izaM$ixXnF_LBi{$ZQ3aLx`oHn-$RzXHE&z+GCXtP%|UzTL#<@ifaWLSvyQCX+d|JK`N{37(kD7q`QP@cF5CM z>5Qj0Ff7<>7B_pYAf0z*$Do~)X6%TNhm}1u+csV=f0g0>&76lyW3uqPd+D`}#*;8D zS&5E5?C>GY))tVnH~th>Q?QNTpaoP7!wP1tz9Ii&$zf;2FKczr9j;>W(lvpKl(6)Q*NCUr5>;GHr?;C)INDAUAuWK?YiYM>!LN8)UkhF@dBJ`g`ZS{Rg15KO9C*@v zYu!Lb<5k(C50iMKep97|qL{s9TaWH-k#;81I(BKJkT#H38~Mi8i$~wLU|IORcq4qb z4ZM0i0+V8oz&jWpTHlzhMcZAH6v(vfPLuQawczC0A<1Saa(2^8JsaFM>^sttc1qHs zm(CV?Quj(cxMs^+USfl^Np^A`N>bq0cALm~#0ati?8x%970ViP%Q1^bqtCEM+n1%$ zHtsv#95nlwGhhETnzEUNR$@gno*tf2_)+;0{_oB7`FKX-@wUi0vczjuHj#5_N!F^A zv!=xXHWfKQRU|H)R*Ol;v^KoUUbBnhlcEwE$6ZqGffIr#CQ1 zCbfknZ8X|-1k($A5>7ND*A}<7kfe<#TM1Z;*uz`0K5din@TB(^o<&;l(l+V~=|f&_ zE`>HpQA_HY4YV4+)XoBA$iW za^I~$nZct0+rr~*5$egNJUj{W=))YI`;-~59YJLz=V|>}gL@XBl`To*ZNc-hrwF#U z@nrF`ycw)H$J{cpC0Xq{VoQDR(P%sib2OfW--~zfKGY^7XV%JJ9_+OCo<+&ABX)6H zq&8|}X6#xSs9;u)AaXCqsgqJ0*qX%!4O&(^ijnVlN z$r`f(xTO@c^tbXH3f>%8dpBn5cbRFMlr4H0SvHVaxLP@6>5wwp$h9?wET-lojsqeG zp`G?yIy@PvPqJ?$o;FrVCW6WDm6f?<@Bke@gGXh0@F-bhjs$_D4}(6`O6IvJ6C{sx zBuYJVM+^A-j5mUKu-f-A@7#ylk4!-Uk7R}(jlhWFxAgb&AL-(emErCa=04Lw_|Qsy zB}EJ2I%|-G5uxbWRV!u$Zx<7Iw9b9Ti=HWN8*h&LzGSbXbnYwOLHJPn`Y#GLGMkHF z@bidPoDni3Fr#|C3_W^Yr%HgY@(-@>zav~#Wzc=i2= zF&}-n-X~!$fU&=+auG^PYYd~tw{JLe;f^>`Y2nk3WJcMO!O_mi&}d^6ufA_RKC(8; z3+=<`j_tKm>%^R6YD!&Ym7c-Ne&eG*UWm?@q%-@L_R*)oD6 z6;7tTWz5^etC%BcXFjz4msyLXt$~l0a9Vnt!ETX>V2;c#4vj{Sb99zCBID88GxOcs z+_Y?JG@eCAAI9+3Xd%3MySmmF$!Mfkew`s{@DW=ouOy?DAX&2H=E_1dq-Drff0lah zE4QHs&l_(RPr|J2yTapZN?HrOJ{*a+cRrMYlC=wDlu7}VUu&6S?bm`Zmz8X>WHsBR zaZtQWc^9Q9P7B`%8?F10#mV6HY$#g4)oyJD*>QV#E#%=v69Ee&(=YKKybrB!%+>%Wfw5qo-`Q?qv&Ea$2^&Ey^5`?21O)xbJv#(ClO8|I5r7`9Wk$Ru_FA#nbHJ z*HG|1`3SycV??hN(B@B6mg{N3v+VYcDLh%;#xagQ zWU>q4k1Ev^S>TcPT!)7jDgSDzYAt-}ddb>pn-nx?yJRcxkF;|g63;^5k^Hm}__f8* zif7pI?qnmBU<_P9!JPUI)o`m0vckn*s(UeBorE)DP@XFHYP4Th>Z2@UE zgSFQ`4MUUVws1U+m)h>poP1t}AH^GOqou>c&@v%2U?Rb9>9RJB(U{Ub zvvf)0wCu8|WtE*O3fmqNQGZ?*CJ=53BqJjurFTGDLH$XYgL^k?ucln+P# zrpjoFl)j_O<3lR9M;|xLg_o5<(2mHlP6jWkCxavL^7FIIIan00zCZdfiKms9Em=<_ za%Y?w;f>gz9gDWJ^k#%wmXo!+(&)2S@9fySmhe0tO@?1f`n(50{QbyG)}j_0t#&$Z z_7SgQj-;LWkd~Cur#~7+WLg)01TWJE=uhUP1r*tRvH@Je>pgm4RAj<86?m@vJdOc#($}86R@I3+qGg>%XK7tR)4Py$^;+6BczdL@qdSnd3n9?ok{xHxT(a01%vOqKYn)%AR(qlgMVh}E z^6*-4J-ikhd-^5*gZE((7&T)?XGj^$C~t&sv|b*Bk-(=IRF0MbJ$O}?3^9kBc({d$Oe`SUqnYVYti_mtV+7}?6dSnFeDpCoh#9e&hyO( z`&w+YaFD=?E}jTxgfF`>WbBJz&VlFg?Gmr@4;{0V5n2Ei;6k}pl?VAWer|8;nUiF9 z)wK|KB+Q7@DPH6Zh~EnhQzI(RuPqy+_C)NXnBJH*Ea@Iga(s*$EwPCy$x7qX#vpB` zk}VlLY9HUQGkDxa3C|j{hgUH#gb(=#v#snYTkb|PHceLJmvr}JA=zvxk{xB*rLji+ z9c|wR9t7D;JD+d#S_*{TyybkB?>XTTn5W2L(R&%*545VwWL+p?IW@g&TB$K!2T%;7$? zN|q&QtJhk(8X?Wl1-s_&xefe1w@0##r_Pi-S&2pm!-`q|q;a+2)z`W3Mi3vfL{gu| zADN>W`zc;_+}d2EYb+yc;c5L@yo%X^r_sUqkWTXYstHiANJi+D;!&cuSpn0-_8h1M ze9x(o@w5hzNEs55=dUeTo;@v?+I-=9B)t~T$ZCM%Wp#KwBk`oQ(Tg{d4#J1ji9EPO z9NaPqTQUqQNViM`irI4Gy#S-}XYnQKvAacI3;!HP6yBV%%ClC?UE)>Dk<^-uSa=u8 zhu;4(OKtVqD;pz=qVh=R0?`uP3m#rJn>`Qg!IZAGu}kFL2Tw-o<930xjlh$}smVkz ztt!RB@$dj0KZ8eQdhjTDm6=CJ9|nDRLYS89)l$SWGD#+rzK+U1HH>I!F9ii_Tu~fO z6xZ{0v@bFVGI%XEWOaBOVFX8e?`?FMd&k#@#@B+U*_XkL%-a^cxyHP=K9oL(=o#8W zqf4nnwc$v4>bY-P2E5OBbI`tzdFMW)(UqsD(b{`-&9CuMn|f?Jv|zaFbEIwJX;`h3 zUEAP$7f&=pkC1ib)QXqU6UDnLe8|f-ejeZj#_U-}l%$&wDM`8Q^_Blg{876sJk6FU zwnUM!W~lXkZNolo{_%E=vOL~g@Y=F-D{7y#@n`(0t>IQ5T6h=Ahgv;P17uH~&na_5 zq`bMbv;Y7DWJyFpRAA#RX6amj{&Aj`b1;q-S*+Pj#+nGyq>1oHb!Pds{w%+?7_4d` zVuR#2DewX@KT0HJPc3-VKCepRGLk+EFXKb${irdI#_RGS|6OM9fKqGEP;|GC&MfZLx3|S6ONQc)%EF|Q zek9%q%#m2q{TXq;dXGISWwKMPPs`5c!m}`2*Xz-TtdYO+2+IwNWFb7dOS=~Bl8um8!g$oeXUTY#fwntms1=Xz_#7{j{Y3G!YD2@*=8LpZd-i&G z)K(3zWy}`cl|J--Q$@-gCDO9Jmdww>9>E*gb+UMw9Wsj3<3sJfMepa__Ek9uI3wna zgx`Y`&Cqs%XXiQ4>owBFWoQdS@21wooIsj|zx$!^NA*T9 z=fLy$E}RdewRIHG;+5XF;4N62bgseM=zHGI7xogeqpdTMC1g7|S?4zrzUQCJB&64x zT;>dj<7;@)1)%wkPiP+4rE5&}Fa6D?GHQ%Cm{C-8}j6@!uHD(X5VqORz zN>7<7X&@u0CEJvOp_YY80ay!$oL}OzCR;Y!ie#RA9GM-~8*2nl%1|wzX?i3Zy^L4# zb?>;nOd(sncx%PNi_EVGp7h?rv#8aF(J@;(sr*iT82yncsb3;4pOM`#j_J?HkD%Vg zy$#F}c-lQ{^|v;MB7snQPuo4b%)HHRGM=2bfwbb)J_}F6Jo+%{L+N*!t;wnNwPZ;a z@95w+C*COR$i&(urgyJLV)g`M2(m4BR)&@FE6q}RvMGr?8}}WLw`sIBA4Z=tZ!5!y z1lE?U41#bFcbDSs?poa4y|}x(TX8J~iWH}~Lvblq+`YIveEWX;?Ckv7+?!02 zdvcPAQBjgXMj$`{003D|R#FXoPyX+Mg8}~@O7FCSZ}85tx^4h~GWg#GkEbHXd0l%93CDXxRN}MT)WEc zoO>NI%O8T3N?3$~E5V{-l&O+eQWBoJlRRi^ZoiW?=t(4#!8D%1q?=lr-qf)&EI8E` zbUJmOy}Z2e#mjQM+h3$`J)7?)*Uf6`_240AlTQM`0}6C~zb$pQMQX2}H<)prTefk! zsajW@O}Qr=ewAi&4aU_;30MeSt3SWFYR?XS;A?wGm*-fdy?0(&&GJu7}WB64@z8y4Y1SY3IjmGZh(L9W9+6jUY#HpvN+#;;2`<|J3gI9V@p^kMoA4 zu`Afkl=7A22Jhbl6)sFp&G@_KE|Ov+n?q9u4$DC1Z&X z0ty(aI&dxoy>t`=4U7!-x>^czO(P>CvL#(zLQ{BELjzi4kbz3J%sQ0MDKaE|`Cc3N zvz!hon-|6m;W|Tj`UocYvq*CgMb)1t6F_({&IrA1g zT5RLmE)I#2Ei)V+ma0OtRjMClq|n66#iOysqjO`9K9}H^A={4b+}Jd(Yys{-nk>g( zHh=}n;^?E>7VwK$kxg? z$L$?)fHko|(8tw%p;B|yEx;_@q`R^XKYO@=krdql${8}a3c?_^3E54U3~37+=m1_{ z_V)Hd4i2ocQw}10=_7`$iW<>P??bgaq^2bnJv22JydWOYf*AmJ^6DHxF-bIWi_HB$ z8m^~EoczHMfFB^}yEl?ztyOCjVC2(&qYB#hD6-9okIq1w`2`3gZ4pfL+|*zZ^(q0I zxZ_MG-3SN;7UL)2qkKxjSz2ZyGJql1rRb1urzQm&$ zf<}R4l8I(!r>s2PxW2xAqxNeu0$D@VFvAiWaL1gXfp~>SuNUWzTy4%`(6_8PDcsiM z4JIXwtk+z;3-j>sxEZrc+ZAE&G-Aw%ZOA%oEPP|6NRj0T_KQA>0^YHJMuC-+vepOe zc9-4_a-=%qd;adfep&4*Du ze`9NhzXpCJ#Mf5?dJN?)U;ea-u3@n9nnd5GN{Cu1N(U#w2}@sckd>p&BRB@8vkmcm zSkR~Y`c4-JUA6SQokbi~lE~&E!b9zKTq6O>rD_S4mG5-u0WiJA?CUGBHjEJQ$3$W9 zh;J^45^{-sB@r`N!<@L$`a~}By%V%%5D*liYtbhv}Cq{BUCnx6xVog!fTJt&!#GAjjBw)Iw zO-a{@&gCL~n8nIVQ6`#-D0hMwTyl1*21wQ^@==f#p?%R550kdJXZwj%;cYDse3bS@ z)dYh+bgih#2l0ZPl9G~~9PR7`7(#|W+vc-U?~sj*YX+HaJ+MaS~7GeX7# z9VO^JE>~cY94nfPr$80(X7`V_LQ4qGb>^Py;jA2<)4`gnW(;BU;^*&*P^&%u)&a3d zxyR!ZK}7ud$xiDwAtBk682u&YXyI(NMHv+ZWy(lewxWs$eHw=de-ixQIV zJhMJSZjtX<5!EuO-hmp!^>{Lad$S2lMX776(6%MMr*F+)DaL9M`#-e>CyO5;(6-i$ zCiQfdqg081ky3VqkA^4j7Gh<+%r2GBY;Pp9oRE_?Yuf^rnjKc1=M;Gs>~cI=&;x2> z?*x*Yg1by1H&5iZV?waZGW;nGk+yZIV@fOMihuzK2xxC*c{DGUn%_Yc58tg&q*Bp# z9w@kP^w8n&gCy z^I$tK(>27pY}%vhTm9us4LSQQx@?|YN)Ez##`Z543fEr|U(%@(%O!lViFbSdt;2ZV zH8eDYSNTdW(HufAO%lty8~4Ev4EmD|xms^$i^$IaA(ts~x)9lOr~lONm)p z@>t@TQ`Jw5v2Q=6-#?NoW|7_1Od1qC_x!X<>qfrZrK)b?q%k~IftAABlNVF<|q8vhwAGy zWw|Wc43!$>IAVD%zy$mJ{QPHqz3a2)LM_@%ZdLYgYv(PRVI=?e1*kwFi%>+_FSgAX zzjt}DcxxC5v=v4(L{SJ{TGvq2OOiUeKf$GIY9m+%l2holoBfUzqERj&TI~tysd9_` zQXI3ZPz9d5 z6%IJGtT&6zH93JFml``%u=X(1TBAUSHH@N;YbbzdoNrbc#k{}_AmVvS*(VTUKz;@`iHmhtO) zGuakoKqu#y?ONl{TRfCj_hO!v(4%wL2!NHQ6dk=4cb{}^!#^lO+(*1XHy=VRsOc$x z+`OA%v-+h%k(&t)^Og-?1{G7XnYa+E+iq%VYVlvceidqA?Q@ii6I241je(GbK6EkW z=;xgk_si{_xYN12mQi0N)%RL;XTF}pvx7D?sZaiy8A1QD$lOx9ZOO*;NJ!`tGJ;2qy=AzX?b{63Z+;fEEqzXV7W=@M#Jk zJ1G{!xMPQmWvh(&h{*6ep;M=-rsiFmWYd_3S;4p&Cs1cS-`~~Ya8Qpt8Lx*%g+J}g z+<3CL5~-nFMrPNFaCoIljn;P8$-QQ2g!FGg>%Q`4#X$`|>8I`B9aEr9x=yiWXtY*E zOZQTNy$AK)jCaB|{ZI@N;CJ;Kcj4}2(YTg&40atmgMEIe%ju##qQ{#pW}Q+i z5}3-R0^l-M5058{q7Nw-(`Q!LcSlndT33>%VQaY8SNjc7+iYe%FyQRO2}le#s#e>vZm}jmrCB9JUkaOOc5QxsFtl&$PSRboE$r>IA-wPE?v4NbEu8Ku*#E{N#i@#8 zZw%OzL-ku1qa;pSjKoF8F1s@O*@^fybIO;1t6zI@es>-5^^oyx0hywm9uLW=$8sDe zo3wKxv(rHUg~}_8(Fd*usvgyU3v&D|@Z%8Qyo$j`F{J^j`>j0$HprUHl!k0okzcS> za8s`sz0IK(i2v@8P2FO_n4i&3JL2q;5Ni1gf!S8e#WpRnh0|7NvE0m~E4BtrBDWBH z_M@rcK>=Z_F%Rc%$(k0)A{>6nbw*;O-$d zJcao%j{f!gZB%C2*6{ti!~&-Sruiv*5$8)8+T@06;>S%+OvP8(O`DK4fo#5AxpAgp8uGPm-5)WgSp) zTqomzv?xnEwwI=~O(WtQx5Hj*pO5lloUdz85=%=$Q=?N3w6yz5C6(~GTov0r0?S~% znA{e^G;G&qV$f*-hJ;XIXMt`2-xaM_iab)L{lvu~j#QZ5Ac*dSMF#nG?%1{yXO0}O z|2cPAFZF9l(s{6hTHUmvHbueJth9`4H<9ws!)N9r%Rgg*URad zj?eOda#G19OPJiPQZ8IYxLr=bqZ2Nyi)vki^lnD4x;4h*2%+hq;dj}8oRNjyZO}pU zSU{C17Xmq2(y++oP2tC@wSvIx{S^To58ERowu#d9O0>%j6p@XE1DW}~ zOAc|_Z?S1dm0@29*H!A%F7-g7Yf#O1Io{@Zhn@6= z4Tql`VDYw_ONIR}atQq0f1w(3xs405zarV(mBNy1QLaB3OJh8~&cMN7sO3#y8~bv7 z*&5}5KUy1SVkC~QAOCNW=3hQjZYeY=2vTIkL${I0gkQiR*b!5XdC48`fW=Y#kk@?q zT>>Yy%s+g5BgKKxOW}7w^c&R}jgqJ~(*R^9^yT{}f>$@8NGlZd7N`O|xvlR})F};u z_7bCF(Yl~b!r1Kfa{VP@a2iMm$CqZG;ZCYsJ@TS@{-eXmAe`08X z@W8urI0QYdnVDyl;aB+RflRxAoNt!LIiAvg~ z!gaCzQ!pv&Fk7)c^a&GmY_a=`e>azYi(`76(E6kJF#!?KY`+}JP}`|?_vFumu4-P9 z9qyR(6qG7#RgyAVZuOVQvu|7sHORWU6pjGRHEEaiolw=*PP10ZuneMxDI3+Cg{P z_qM+LdIc;B4)cqP-#a?|s*7n2_dOH)LJnbhVJP~4qt&2{VC^5y!r>zCk0oyyJpd4C2I}FrBCi>!2o;=TOZICJT*yvqmj3iQ zKnitP5T{vwv-jJA4-N2CK6+dVq03Xc1_e;$SqiF8(bZm*fo(}DZ$hP+%``tdd%Xcp zKD}Y6!nD@5Sm~m=R{u^18ena z105{FA3fQb;3LXSFr@Q^=@6H1KKrdsg?cSS(Bs~9uvfH!9cGsEY<6mDYGrClsaL>{ zOBrO@CO^m7)Y9U`Hq`L13Tt;+vBNx=G7lh{p!CzM8+WZ*w#=|SZ7_hXaniiJaztKv zs%mTC&EHuPu`J;sQlMmkI20gi)##o}6wGh9+_9)EzA=mU_yrbdo0Gvb_FXJQv=~hs zjIwm6@x~*m_}AzaFbnY)k=&l`B7629zO22i?Nvo(JJD>XaHGdA zY#CIdk_EyZ%?P^et(vv-Os`mDw`LzQE`tgSa105=fe^Ij%P}b7apBK79I==(Z_-AwZGd_u|9{S zg2^RcdK_vBfe*_h*06~GMF$qtm0!r0Ty9Gcj#eFC-*EWIM=Y`O;u^PYz{2Z5k}Hi> zgf1$de3@8-IN+RN3VT&!d-Y8$T{G>}v~3|PlizJQOOp4y8`6)Ty092$9z!}+XvJK+ zG3hw68k%5!g&#|*f~7?rrVOeq74&4bD4}_UVFMVx@uOyP>u zTbKD1GWc@dk))fkKgU@;V5nLVBAEvHrqeqlB8~<>k~DE)x&T+(!R{ zr{E%SZ}t3Gg5TzQKYyNEqub|kL&K=!Fdk>;GjS@O)G^9;U39-#p-?@)1p+GAQlsWY zYLGGT5+;HLZ68MTNX~4SZ8jyQ6WMQyfsy+O;fN}i7JanULphvYg{|>>;{PV<^8?`` zfT>wObj&h{92k6m>fG)3}{e!;MC(35g3+mkuL}(^mX|7<_Ym^&HhitAaw8S2vL|~*@D<_EVuwE7`K$l2*~`Od7LIfdEbAND!@@}> z=JQy87?MRLH5kG#@gpH7uw>^whye|XF+xF|N~l>TvJAz|x*%pQXl>m-6Z7Xq0;>6E zgciS=u#ohcRhxM@h*g-e@%eC>p!SlqC@&lizFP!)vLPL~;K~!jRjcTHF!hOdeZ1U06+8bNo8BSj%E_l4`}*Uz zrV&|RcYg}~AvO^{Uc;cvZMvV3#OZc^iokTIf_K2#Fmb_9rDhdbIB@7c^yo!?U>dGa zqjbRL8fVXeD z`%pUB_4})3+o3+fq`{)ouEJ*%y~Hhn=TWP#6Tfh;?a$0Pknk*i7Rw28boP3kt?W<5 zO=XHEBqWIDgzW5dm$QcyQTrh{`)W4O-kaei{rueu5l`dlU=V%L513GmvN_OfFQ*dR ztI49K8!}dK$S5!!=10W(7k7SbLBu!O0uu{4x}Z`uIyiELInG8Tjidcid)e_zy00!B zhC7SfWwZJr{xoHQbTRjalW4HL0+KW8*4tg)){jcGOM+Q)KKq`iVon2;7@MOUYKC zZbaHkL>#&NDoX!n*<{!;7B93-58=<=zr+#jj zM6?mi(Sm$v04fbKYB;&zs~b!nQ&PnWp1kX=B;#<{E|Lq`u+IJT_V11XvT**0I@%(Q zy+Im`7`CalY#-bMQ4&a@kJlRmI8%a1mle)Qi+c@0DwX=dno`B^GGO0&0b+{g$B`e= z@o>l{itDYSiS+-)FNv6L|0y|kc}xB5mbv<7hw9-u!)c2&F1{aig*+e&mZb-08hDet zIk9>{w}dpnsyK&47@3m)?MrjW;BVyKP=em4=+-WVy$bs2U^8ii1O>4!8oJ?#R$}ooJf=z+tAakCI#roK%YtSNxN{_iEoL{{%=Wt zJLEdwkW($IHQ%CW{+lDJ3ixqQ;4}YpHy-1iL8TbS;$0FoU!c#}NalpklWL90Hd5aZ zHW$Lmnj7LYsJ%wU-H;R!Caj>b)w`Sb?D%-QUdLWz@ow8J9T)S8p3+_qCDUu`HyJZ3 z=<*cK@6AgBG<|=#YINvEHnY|H5l9&^ij|26y-~TDcp*yfcALhr^mq^b9&5!s;}O7% z6PIv*d+V*#zfBjiva-S|BqRiO5E;Hh%aizV0eIn6OiYl{f<~~*XE^TnN3O)L9;V)A zATSq-shmH5gD+|Ek=IYmE9=xdkJRY!hP~P1@-8@$vEKL~C94x;MZJ z_7Zo)q`}=Qi1CKoWf%+xcPy!!(zkcihA7%ANOtq%$~(cjz=kJjNO z61LB=vS<#GBn-rEHKz>O@}<#YjjL%g`95^?bMi@W@iA$Rx_dKMd@Ni(8BRM$mLwxg zdLtu{%)f{u*aVtmfglncK58H;0?d-jkq>>J&Ae~h9G zqIlbH^8g>>-3mRTZEdL6O zr!(0?347u*@c$ZrLX+j>yHt+yI2{WtovH3#6Um0T3Jzt;}$Uj1h#ZY-Ei2fn(W(9Z12=pF~;IoiL_x^Gb;tN5pb z7Vb`qVuHMxBI-*EA8ED8 zCEdnZ0ZYiB_7J207cUGag;pzbXey7hZU(x&wvXmF{ zL8V75TF~g_6EE!G7P~RJc-{(F{>PDw$Ukbj^PyGC{QUg2hM%&W1OZ$)aS;q`^qJg{ zEkDQO9i}~l*L)iIzyDl$e(_?r*UQ+%KUDu5)faL>GJEwTL`Rg=I-g8gDHz5NNkxq@ z!{;Oq~d?1b*EDzjPugb7g^z`&>9}XQI?+5$&%kyFn3dLt>ij**Q zb#=(|qFaR!NuKj|@uDh-wjYS1HRkpQa$ZX2XpjFAkS>h!(*zGG%iKelVBPHl`C=&3 z@T-={BW@srZN1Amp89Ssr*NR*y=vjWb#kc%VY@BwVOCBKL9`?VYz*zwU2PEYWdS9B z$Bw0`8}RygwTp+3zg?kG>1APIp-i9dx+gcSm2TjPzOB3gdel6Yb1P>Ch16B1UmjH> z@w8_h`j1J*=k{_yQoo4h!Uh_x`u``=_5i3E<=#R&rx^< zBz$xJHi1x$lphDM(<_)D$Q*$HOi$61C~OhJx(f!Z^{2=smb^X5iDlclw;e8f zN+r73Hx82!D5;q?lgPfz#&u{v*MF;EKTeBzBT^_;i$WTpwbf9PV>;jL+3bA19#6V^ zcyKaqcR&6$IXQXfD5pVBNr%)Lh-|uO3@7+3u$A7&*=W#+^tdH0F0(%2fhD1U`|J>pLR`2zJWyF*!Jwl6TSuzmyt1kCDK5$Tj6_CM{R-_H%hOXzxO-*$fGon(F* zX=1+Ig#@9#!pijLcjq48V5Wiejo7cc-ola3!nv5jHJQS(6~lkug&zQa@W`MRZXdae z-Gx186*$JT!+R@eWiG-+m9ut#u_YGB6})$6zQVg^CMBowJ2UN9;h>DB-u18T?tB_; zDl6EYvkSxKNV3%RcXqCea_gREjb8$9Ri0nz{iw#gMl$;-lF-pa94-4^= zJUBS$#GkN2jDR3_IB0qPO;%M{NP_jagOWJsNl_#5de|HRl%@6t6>NFC0C44^5ypBN|}ro|FW{d!^ek@PfWZnB`Fhu_KeK47MyA1J4&5qUEwwI zuXCA?-TCXf*Jc0^>=g8}Y6|p_5D|6$tf@KwQmZRi8KQQI@na7n@HwaVigVwbT$C6=jXD9NFt2{*8Y8m5wdg?4o_1Rb@u-uY*!ndr!m?rwZZB0FFx=DYrOz9(mLa_aF zI1V1%77Gz$A>c{dV=H99f$IOngurc#A(|%Qwc2?uR?(&p9s-D>NEwL(hJ;_YQTzCY$nX*=A&}qa^SbPe_Vx9#9(LNlST-lGX8(omerf2X2|~$BB%|@T4t0XEw)&2X zoNR?dO-wn5mWQ-2MVZ;wGP>*;LRNXhTVWUzQma?AL#Q3)tQ!F#(IEm03p+rgl;^Dm zSI2~;Z6J=5&{f{wF8vM`{Qm_R@NT~B0^HqkGrj{ExK|3j=88e zcah78!{8L!Efr)(+?77wR5k`ozY311-;7oGau|G$MY+;767)BfGQuL;!qhDr=w{wq z*JL%0+T#dbt)@BD;B26rkd*X6&^t3aD)$ycQ;_8$gfIrzBM9!>E;ZSv)!42`auPV$ z+t}!+YHQ;{T{C@{wYOohh()6HCJJwbCAmtbU6VX0PBY&8yIZ=qU)7YZAqJz?C@pP^|CnE9DPngi3PNU>N7mXdO(~ zMRdK4j>?Ss)D4z)1lay}|A2RAmv@=_F)8}=9WZ3C1>@>7KuHK}M9a;N;wRCXLP+D6 zmBq&z!;T%m2%nJf#nRY#^U|QIVTRV(9Z5-G=o?CjM-+jZkHC3nZYSaC^;84T{CWm# z*ulc?#$oByny3EruJ0nV+SQ~Tr1xsE{{bD(B6#%e_U;ZjDIwtv)XR`z`P=Js3BsFj zn)`aqi%{w&B=!^*xCgh{T__0l^FH*!E2=|6LYgm_6c-!iuQH@&CI&cv4ilV~sbrDY z?-fWBc~3pm^pPzHy~8RdAFkbyd90H6U%Ia$t6?DX8JoPr$HT)rIXUr*b$fS`W^+@Sikjl8+(;E;=zODJl7byTH#?x%>V3BucGfOxW8P zj-1qa`q#(=!0-lm`)ST9x@B|uoH%w2sU{gx%f)A}&}-()S$`Ci(!WnriwM_GMJ3;Q zHm#9;GsyT{dii`(OPZ6F<&Dw1k&=S`HYIQY6$=Ib39CdkS^bi|RbvYAkA-cE)27me z-{wmyAZ=E!^Lg=(OHok~Ij2mOtBVybWJ+(m9E9%NAH=k|8%en(R8bsU)oLBAQ zWgCOP_q5utfANqdpOl@PF1C7md#9PkpXm<$FXN0~i@CVDtYWp5&t0np2{MJHfa^KZDuuU$UfY;^(%C zdPkx{fW$|2>*>2_5bsSFJCI1lGG;Ha0dq|yuW6Ha>mh6+N4f2Q%jUm>fnAlelsCm{ zh6j^KDnhahe#DsK(HCx059=<&#MlD7jvDXMQ;_vKj7G-BK`L-p(_LL%;69`sbN8c} zvWFI(J&eIDl%PB1A2%59Z>URwc#rF3&Q|<~RF=qwt{Q-Kzjmak6q(|Z@?tWi#Lw5hjTL}b2vef{m6gD4q0GAYU$DK(`PYjZ7uj?B!NQs z<11sItL^j43nf^|j-9JV)EFC<;1P~#LG^M(#EoG!n*+&+<)sEoL>wl)dD`$+gx;vX z)%p(w;8C}~tHvKpCH+>pDZH3Sh`e0To$hK|*_5IeI?q+JnDAtkaSGQ$2<7xi?0_Ul z47nT;m=x;|B`|SdFrCAQl*h&3Kbd+D9?_tWcz=H%0f)oA6F~B**ic#I&3zA6m8T~)eGx0(R3hV$sL;nqdJ{X zR&FL%BIv}3kUm;gEIAGh4dqoOHp5;lkOA6kV=dexmUy*s(GcwX6Fj2bTv`Q74Gp6m z6=Nl24k)tFu|BLXq9opL^QX;$ z1_(k_ULLXgW&NT-gB}~?oL^?!!er2TNleJ&Xb+~3G|h`Xn_S+uoO{F-<$~HjO|Rw) z^F*Ngf6htqj}@&>Iv6}-h8?xQ028l-LIKaWjghAIkeB@_lrlT0BCo7TlCe|s25j~? z_dC2D{?v3D8lYuw@ZgXMo-{ZLGNLy&S5|!Oyu7@u<>lilD}P&K{8F713&sWU9^w%a za-fn3dVjIEKV>5y>~Wq#y}Q33y6E``EUL2-OKH%Z(O^pE#Uu%ObukEfyOXBHLU5Z> zrcRR+W@DR?l}tcJMy4Ns32u^+=&>$fWE>X{3JSXZ1J26SG&F^1;;)jbUk7@gCY{v4 z8?Afe)WV`-j41mOGN@K(x5$haY09kEWYd3mgM8dhlAV=h!>Z^eCLs}~<-pUkGW6&# z#-2U6JrK1klSt&lN0Rgr?)Hv#_&TDlEL7^W*=;EKeWuZq+079WyC3Xpy7Kb!l)(vr z@ci1E|JlEVDgj((gQMIy@0kc-j??A&*7ogkd*Da^_GJ*r8>?5Cd~olqWiwDon8H=- zQ*t~wb4f57wsS7b&vPPdX|QL<7xKALQ-#~0K!I|m4;kdxHU1q>3=#5cqluH@#BDFt zn_n#&2L%{2;`ph-UEu|5r;S?NjvK$Ti?|(3@bU4yICQ=^bogz|i#wgl=}jS|rKN4< z#d)0kJ4cSj8Nya4A*y&TR%r_tV(Y8UO_pJ*Yv-0 zY#=NwtOIG_kp}osl9W-a(0Cn;!Q8nTCS{pdpnE8%$Z;#{w{Z9H*xvBFumlf$LXZy< z0E_4||5?C$`FekxFgG_ho7lgJ&A4R1fIR&(IT6ZDcX#*b+z;goZ1GouUPSTeg8KUU zJ+K{pzysxN!jo;NR8uhe&Guq4GEu0a48eE61~C#$?Z2ZVoxckUnwzt;{Rqo43tIXS zp*{;Wn~LT+)I-C=PrbdO7T(^EIut37Ou@z=UN{r?!>N2QvHl|#6ueNJjf){G4Tf^v z*XgyabDWUz^`8J{%HkIAw4c!&nB=Ut7)@2Nvpa#N2?$5ybvr;(%Hu^RB_t$Fl>RON zaSGIBx&<1je%Y;0_%C`d?yN`=A!8gN$#!MG7z7vhxiG7=_#f9mxqgs>E!6h#u3~?1|Nd`4m6Y6 zC2DjEN=jz_{syEZBq+)X3Pc==Hvsw>;4BO0LQjw|)P0kGahYix_+(J%s5+qy<9f6* znagATI~My7X97IAIKH$Vm#`OeWY5o50aQJ3lki_r=c6S|O^=r8(YZ6&o!4 z09;L7ff?L-NeY*VhM$|DN8(^Gbf=?;C5-GN_1=Qe;Ba%UaW-O2_S-y|%w0%l&}OHo z)YSaNu-jG*=}d>tR8duBXVmHK65!_6SX)*W2@Qg<$$=SK%HxPb17%qDu_FDajk5BM zGw)CEK(uu6j1Bk|5~5xPz=*P#WR4Ry9Ic*GCh_tGoGS95AQ}4nEy)O>@2R&)z#IOeI{rjqJWW)9Q)^V zC2)oVe`6#;^*ZsxyDo0NzWjV#TwK|*R6`1LMB*!P{GhEYv>jgb2pMVVR4}|b01r-I z-p(m&|5ZPPbG^`DqO2F6_(!+a?!i!1T^%uf7*=!AElP%lCO!{&1pI+Ffe%iPuUC>% z<>yqiTi~qL(p(6hKD+_z&V`3WfrrFiI1Q_*`OTI9>(fJptbS!y8TCA=bg9%)i`m1QSIYSkkWt-zGNW$}dh8C=Pq>$ge#y7tM)B{bj zdf8QuYTUsLCjF&QXvE~~jI8XkWApe503?Y8BKrUV{@JC1^qG`}Av^cVaOkkfdwIE) zmH))y9CdNP`3mX0?XbXX(QE3z7;W34`lG;k;WO=r7Wdm3ylYc%1fQnLGmFt;nSE1< zK5j?JRZ_*OQ%J=)%#%-(b}pQb@)n%hz2XhJThMmd8H6wRTbpVAAcWM5lJt8JBGk{TPIl>|hT0i1G-2 P6AX}(Qj)9@Hwpb8pbK#N literal 0 HcmV?d00001 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 new file mode 100644 index 0000000000000000000000000000000000000000..2cbe6eaf1e94d22a655131ef5ddae7ab5917fc55 GIT binary patch literal 2091 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 SCmh2&11QO<%T`L82mcS4z>B&7 literal 8908 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 ATmS$7 literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/values/ic_launcher_background.xml b/flutter/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..ab9832824 --- /dev/null +++ b/flutter/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #ffffff + \ No newline at end of file From 5dc0c5be5e2dfad0b99cf7c65927e8812242e403 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Mon, 13 Feb 2023 14:58:52 +0100 Subject: [PATCH 101/202] invert color of checkmark in darkmode --- flutter/lib/common.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 85aae4c80..e1dd1a1f8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -217,6 +217,9 @@ class MyTheme { style: ButtonStyle(splashFactory: NoSplash.splashFactory), ) : null, + checkboxTheme: const CheckboxThemeData( + checkColor: MaterialStatePropertyAll(dark) + ), ).copyWith( extensions: >[ ColorThemeExtension.dark, From 7dfe20417ed75264e86c12660a85d23507cd603b Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:34:46 +0100 Subject: [PATCH 102/202] Update de.rs --- src/lang/de.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 1672af2b9..38f4fddab 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen"), ("Enable remote configuration modification", "Änderung der Konfiguration aus der Ferne zulassen"), ("Run without install", "Ohne Installation ausführen"), - ("Connect via relay", ""), + ("Connect via relay", "Verbindung über Relay-Server"), ("Always connect via relay", "Immer über Relay-Server verbinden"), ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), @@ -272,21 +272,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Total", "Gesamt"), ("items", "Einträge"), ("Selected", "Ausgewählt"), - ("Screen Capture", "Bildschirmzugr."), - ("Input Control", "Eingabezugriff"), - ("Audio Capture", "Audiozugriff"), - ("File Connection", "Dateizugriff"), + ("Screen Capture", "Bildschirmaufnahme"), + ("Input Control", "Eingabesteuerung"), + ("Audio Capture", "Audioaufnahme"), + ("File Connection", "Dateiverbindung"), ("Screen Connection", "Bildschirmanschluss"), ("Do you accept?", "Verbindung zulassen?"), ("Open System Setting", "Systemeinstellung öffnen"), ("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"), ("android_input_permission_tip1", "Damit ein entferntes Gerät Ihr Android-Gerät steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."), - ("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen Sie [Installierte Dienste] und schalten Sie den Dienst [RustDesk Input] ein."), + ("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen Sie \"Installierte Dienste\" und schalten Sie den Dienst \"RustDesk Input\" ein."), ("android_new_connection_tip", "möchte ihr Gerät steuern."), ("android_service_will_start_tip", "Durch das Aktivieren der Bildschirmfreigabe wird der Dienst automatisch gestartet, sodass andere Geräte dieses Android-Gerät steuern können."), ("android_stop_service_tip", "Durch das Deaktivieren des Dienstes werden automatisch alle hergestellten Verbindungen getrennt."), ("android_version_audio_tip", "Ihre Android-Version unterstützt keine Audioaufnahme, bitte aktualisieren Sie auf Android 10 oder höher, falls möglich."), - ("android_start_service_tip", "Tippen Sie auf [Dienst aktivieren] oder aktivieren Sie die Berechtigung [Bildschirmzugr.], um den Bildschirmfreigabedienst zu starten."), + ("android_start_service_tip", "Tippen Sie auf \"Dienst aktivieren\" oder aktivieren Sie die Berechtigung \"Bildschirmaufnahme\", um den Bildschirmfreigabedienst zu starten."), ("Account", "Konto"), ("Overwrite", "Überschreiben"), ("This file exists, skip or overwrite this file?", "Diese Datei existiert; überspringen oder überschreiben?"), @@ -386,7 +386,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."), ("JumpLink", "View"), - ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Peer-Seite)."), + ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Gegenseite)."), ("Show RustDesk", "RustDesk anzeigen"), ("This PC", "Dieser PC"), ("or", "oder"), @@ -449,7 +449,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Sprachanruf"), ("Text chat", "Text-Chat"), ("Stop voice call", "Sprachanruf beenden"), - ("relay_hint_tip", ""), - ("Reconnect", ""), + ("relay_hint_tip", "Wenn eine direkte Verbindung nicht möglich ist, können Sie versuchen, eine Verbindung über einen Relay-Server herzustellen. \nWenn Sie eine Relay-Verbindung beim ersten Versuch herstellen möchten, können Sie das Suffix \"/r\" an die ID anhängen oder die Option \"Immer über Relay-Server verbinden\" auf der Gegenstelle auswählen."), + ("Reconnect", "Erneut verbinden"), ].iter().cloned().collect(); } From 116649eaf2dede3874a50ba8a27568249da94e3a Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sat, 18 Feb 2023 09:42:40 +0800 Subject: [PATCH 103/202] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index c2d92097c..a955c2a2e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,14 +1,5 @@ name: 🐞 Bug report description: Thanks for taking the time to fill out this bug report! Please fill the form in **English** -title: "[Bug] " -body: - - type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue related to this already exists. - options: - - label: I have searched the existing issues - required: true - type: textarea id: desc attributes: From 38cb44a89c17f605d714c3b5f70e708f64812546 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 18 Feb 2023 09:44:45 +0800 Subject: [PATCH 104/202] remove title and checkbox in issue template because title cause guy empty title and no body care about the checkbox`` --- .github/ISSUE_TEMPLATE/bug_report.yaml | 1 + .github/ISSUE_TEMPLATE/feature_request.yaml | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index a955c2a2e..ec23aa7a9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,5 +1,6 @@ name: 🐞 Bug report description: Thanks for taking the time to fill out this bug report! Please fill the form in **English** +body: - type: textarea id: desc attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 50cd6d0cf..29b0d0e0f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,15 +1,6 @@ name: 🛠️ Feature request description: Suggest an idea for RustDesk -title: "[FR] " body: - - type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue related to this already exists. - options: - - label: I have searched the existing issues - required: true - - type: textarea id: desc attributes: From 3fca9c166187abcfa89ad68d96fb77880ce467e1 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Sat, 18 Feb 2023 19:43:51 +0530 Subject: [PATCH 105/202] docker file --- .devcontainer/Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 0381ff966..a96c782d7 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,4 @@ FROM debian - WORKDIR / RUN apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev @@ -15,5 +14,10 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh RUN chmod +x rustup.sh RUN ./rustup.sh -y +# Install Flutter +RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.3-stable.tar.xz +RUN tar xf flutter_linux_3.7.3-stable.tar.xz +RUN export PATH="$PATH:/home/user/flutter/bin" + USER root ENV HOME=/home/user From df8c7b1c3096eff65da615cdad08d69d00df11a5 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Sun, 12 Feb 2023 12:59:51 +0100 Subject: [PATCH 106/202] remove boxed layout of nested option --- .../desktop/pages/desktop_setting_page.dart | 94 +++++++------------ 1 file changed, 36 insertions(+), 58 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 25c485a2a..34398dd0d 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -650,7 +650,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { context, onChanged != null)), ), ], - ).paddingSymmetric(horizontal: 10), + ).paddingOnly(right: 10), onTap: () => onChanged?.call(value), )) .toList(); @@ -675,6 +675,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { if (usePassword) radios[0], if (usePassword) _SubLabeledWidget( + context, 'One-time password length', Row( children: [ @@ -756,9 +757,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { controller.text = data['port'].toString(); return Offstage( offstage: !enabled, - child: Row(children: [ - _SubLabeledWidget( - 'Port', + child: _SubLabeledWidget( + context, + 'Port', + Row(children: [ SizedBox( width: 80, child: TextField( @@ -772,28 +774,29 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { textAlign: TextAlign.end, decoration: const InputDecoration( hintText: '21118', - border: InputBorder.none, - contentPadding: EdgeInsets.only(right: 5), + border: OutlineInputBorder(), + contentPadding: + EdgeInsets.only(bottom: 10, top: 10, right: 10), isCollapsed: true, ), - ), + ).marginOnly(right: 15), ), - enabled: enabled && !locked, - ).marginOnly(left: 5), - Obx(() => ElevatedButton( - onPressed: applyEnabled.value && enabled && !locked - ? () async { - applyEnabled.value = false; - await bind.mainSetOption( - key: 'direct-access-port', - value: controller.text); - } - : null, - child: Text( - translate('Apply'), - ), - ).marginOnly(left: 20)) - ]), + Obx(() => ElevatedButton( + onPressed: applyEnabled.value && enabled && !locked + ? () async { + applyEnabled.value = false; + await bind.mainSetOption( + key: 'direct-access-port', + value: controller.text); + } + : null, + child: Text( + translate('Apply'), + ), + )) + ]), + enabled: enabled && !locked, + ), ); }, ), @@ -1614,43 +1617,18 @@ Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) { } // ignore: non_constant_identifier_names -Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) { - RxBool hover = false.obs; +Widget _SubLabeledWidget(BuildContext context, String label, Widget child, + {bool enabled = true}) { return Row( children: [ - MouseRegion( - onEnter: (_) => hover.value = true, - onExit: (_) => hover.value = false, - child: Obx( - () { - return Container( - height: 32, - decoration: BoxDecoration( - border: Border.all( - color: hover.value && enabled - ? const Color(0xFFD7D7D7) - : const Color(0xFFCBCBCB), - width: hover.value && enabled ? 2 : 1)), - child: Row( - children: [ - Container( - height: 28, - color: (hover.value && enabled) - ? const Color(0xFFD7D7D7) - : const Color(0xFFCBCBCB), - alignment: Alignment.center, - padding: const EdgeInsets.symmetric( - horizontal: 5, vertical: 2), - child: Text( - '${translate(label)}: ', - style: const TextStyle(fontWeight: FontWeight.w300), - ), - ).paddingAll(2), - child, - ], - )); - }, - )), + Text( + '${translate(label)}: ', + style: TextStyle(color: _disabledTextColor(context, enabled)), + ), + SizedBox( + width: 10, + ), + child, ], ).marginOnly(left: _kContentHSubMargin); } From 6ba2515b560a3c84384fb3478bcbf1b2a0d1250d Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Sat, 18 Feb 2023 20:47:11 +0530 Subject: [PATCH 107/202] updated --- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a96c782d7..86c11ccf6 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM debian +FROM mcr.microsoft.com/devcontainers/base:ubuntu WORKDIR / RUN apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cc348f38f..426127fd9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,8 @@ { "name": "rustdesk", "build": { - "dockerfile": "Dockerfile" + "dockerfile": "./Dockerfile", + "context": "." }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/user/rustdesk,type=bind,consistency=cache", "workspaceFolder": "/home/user/rustdesk", From 7dc0cefeee2eb5e6ee3899b0ed63c200dc82ba85 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 18 Feb 2023 23:34:28 +0800 Subject: [PATCH 108/202] fix #3257 and opt svg --- flutter/assets/GitHub.svg | 2 +- flutter/assets/Google.svg | 2 +- flutter/assets/Okta.svg | 31 +-------------------------- flutter/assets/actions.svg | 3 +-- flutter/assets/actions_mobile.svg | 3 +-- flutter/assets/android.svg | 2 +- flutter/assets/call_end.svg | 3 +-- flutter/assets/call_wait.svg | 3 +-- flutter/assets/chat.svg | 3 +-- flutter/assets/close.svg | 3 +-- flutter/assets/display.svg | 3 +-- flutter/assets/fullscreen.svg | 3 +-- flutter/assets/fullscreen_exit.svg | 3 +-- flutter/assets/insecure.svg | 2 +- flutter/assets/insecure_relay.svg | 2 +- flutter/assets/kb_layout_iso.svg | 2 +- flutter/assets/kb_layout_not_iso.svg | 2 +- flutter/assets/keyboard.svg | 3 +-- flutter/assets/linux.svg | 3 +-- flutter/assets/logo.svg | 2 +- flutter/assets/mac.svg | 2 +- flutter/assets/pinned.svg | 3 +-- flutter/assets/rec.svg | 3 +-- flutter/assets/record_screen.svg | 25 +-------------------- flutter/assets/secure.svg | 4 +--- flutter/assets/secure_relay.svg | 2 +- flutter/assets/unpinned.svg | 3 +-- flutter/assets/voice_call.svg | 2 +- flutter/assets/voice_call_waiting.svg | 2 +- flutter/assets/win.svg | 2 +- 30 files changed, 30 insertions(+), 98 deletions(-) diff --git a/flutter/assets/GitHub.svg b/flutter/assets/GitHub.svg index a5bd1de81..ef0bb12a7 100644 --- a/flutter/assets/GitHub.svg +++ b/flutter/assets/GitHub.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/Google.svg b/flutter/assets/Google.svg index b7bb2f42f..df394a84f 100644 --- a/flutter/assets/Google.svg +++ b/flutter/assets/Google.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/Okta.svg b/flutter/assets/Okta.svg index 0fa45b93d..931e72844 100644 --- a/flutter/assets/Okta.svg +++ b/flutter/assets/Okta.svg @@ -1,30 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/flutter/assets/actions.svg b/flutter/assets/actions.svg index 5403853db..3049f3b89 100644 --- a/flutter/assets/actions.svg +++ b/flutter/assets/actions.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/actions_mobile.svg b/flutter/assets/actions_mobile.svg index 6aed6053e..4185945e1 100644 --- a/flutter/assets/actions_mobile.svg +++ b/flutter/assets/actions_mobile.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/android.svg b/flutter/assets/android.svg index e46dab11e..6fd89c9ab 100644 --- a/flutter/assets/android.svg +++ b/flutter/assets/android.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/call_end.svg b/flutter/assets/call_end.svg index 39367c3c5..7c07ee25d 100644 --- a/flutter/assets/call_end.svg +++ b/flutter/assets/call_end.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/call_wait.svg b/flutter/assets/call_wait.svg index 42a11fe56..530f12a97 100644 --- a/flutter/assets/call_wait.svg +++ b/flutter/assets/call_wait.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg index 7088107b0..c4ab3c92d 100644 --- a/flutter/assets/chat.svg +++ b/flutter/assets/chat.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/close.svg b/flutter/assets/close.svg index 7488acc9f..fb18eabd2 100644 --- a/flutter/assets/close.svg +++ b/flutter/assets/close.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/display.svg b/flutter/assets/display.svg index b5a88106e..9d107d699 100644 --- a/flutter/assets/display.svg +++ b/flutter/assets/display.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/fullscreen.svg b/flutter/assets/fullscreen.svg index cd01f93f9..93f27bf7b 100644 --- a/flutter/assets/fullscreen.svg +++ b/flutter/assets/fullscreen.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/fullscreen_exit.svg b/flutter/assets/fullscreen_exit.svg index 8d4414897..f244631fe 100644 --- a/flutter/assets/fullscreen_exit.svg +++ b/flutter/assets/fullscreen_exit.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/insecure.svg b/flutter/assets/insecure.svg index 37bb196e3..5a344dd04 100644 --- a/flutter/assets/insecure.svg +++ b/flutter/assets/insecure.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/insecure_relay.svg b/flutter/assets/insecure_relay.svg index f08bee6a6..17b474e6e 100644 --- a/flutter/assets/insecure_relay.svg +++ b/flutter/assets/insecure_relay.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/kb_layout_iso.svg b/flutter/assets/kb_layout_iso.svg index 69f0c96cb..163e045e1 100644 --- a/flutter/assets/kb_layout_iso.svg +++ b/flutter/assets/kb_layout_iso.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/kb_layout_not_iso.svg b/flutter/assets/kb_layout_not_iso.svg index 09a055be3..cfbb046ca 100644 --- a/flutter/assets/kb_layout_not_iso.svg +++ b/flutter/assets/kb_layout_not_iso.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg index d5481d7a1..d72033f6d 100644 --- a/flutter/assets/keyboard.svg +++ b/flutter/assets/keyboard.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg index 1738a02ee..2c3697be9 100644 --- a/flutter/assets/linux.svg +++ b/flutter/assets/linux.svg @@ -1,2 +1 @@ - - + \ No newline at end of file diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg index 13eb73f22..4d43f8bcd 100644 --- a/flutter/assets/logo.svg +++ b/flutter/assets/logo.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/mac.svg b/flutter/assets/mac.svg index 8092b3af3..ccf9c7aab 100644 --- a/flutter/assets/mac.svg +++ b/flutter/assets/mac.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/pinned.svg b/flutter/assets/pinned.svg index dd718b96a..a8715011b 100644 --- a/flutter/assets/pinned.svg +++ b/flutter/assets/pinned.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/rec.svg b/flutter/assets/rec.svg index 33a57e9d0..09aa55e2a 100644 --- a/flutter/assets/rec.svg +++ b/flutter/assets/rec.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/record_screen.svg b/flutter/assets/record_screen.svg index e1b962124..bbd948c73 100644 --- a/flutter/assets/record_screen.svg +++ b/flutter/assets/record_screen.svg @@ -1,24 +1 @@ - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/secure.svg b/flutter/assets/secure.svg index 29e1d3c4f..fcd99f2f5 100644 --- a/flutter/assets/secure.svg +++ b/flutter/assets/secure.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/flutter/assets/secure_relay.svg b/flutter/assets/secure_relay.svg index 8ecbdb47b..af54808a8 100644 --- a/flutter/assets/secure_relay.svg +++ b/flutter/assets/secure_relay.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/unpinned.svg b/flutter/assets/unpinned.svg index 9e9e3de8b..7e93a7a35 100644 --- a/flutter/assets/unpinned.svg +++ b/flutter/assets/unpinned.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/voice_call.svg b/flutter/assets/voice_call.svg index 5654befc7..bf90ec958 100644 --- a/flutter/assets/voice_call.svg +++ b/flutter/assets/voice_call.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/voice_call_waiting.svg b/flutter/assets/voice_call_waiting.svg index fd8334f92..f1771c3fd 100644 --- a/flutter/assets/voice_call_waiting.svg +++ b/flutter/assets/voice_call_waiting.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/win.svg b/flutter/assets/win.svg index 326f7829d..a0f7e3def 100644 --- a/flutter/assets/win.svg +++ b/flutter/assets/win.svg @@ -1 +1 @@ - + \ No newline at end of file From 11d5cdb4f119f0fc523cce61bf6ce67cd2013777 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 18 Feb 2023 23:24:29 +0100 Subject: [PATCH 109/202] Update README-DE.md - Translation improved - Missing parts from the english readme added --- docs/README-DE.md | 115 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 81 insertions(+), 34 deletions(-) diff --git a/docs/README-DE.md b/docs/README-DE.md index e537d41f3..8ee4a51fa 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -1,63 +1,84 @@

RustDesk - Your remote desktop
-
Server • - Kompilieren • + Server • + KompilierenDockerDateistrukturScreenshots
- [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
- Wir brauchen deine Hilfe, um diese README Datei zu verbessern und zu aktualisieren + [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk]
+ Wir brauchen deine Hilfe, um dieses README, die RustDesk-Benutzeroberfläche und die Dokumentation in deine Muttersprache zu übersetzen.

Rede mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out-of-the-box ohne besondere Konfiguration funktioniert. Du hast die volle Kontrolle über deine Daten und musst dir keine Sorgen um die Sicherheit machen. Du kannst unseren Rendezvous/Relay Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). +RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the box ohne besondere Konfiguration funktioniert. Du hast die volle Kontrolle über deine Daten und musst dir keine Sorgen um die Sicherheit machen. Du kannst unseren Rendezvous/Relay-Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Unterstützung beim Start brauchst. -[**PROGRAMM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) +[**Wie arbeitet RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) -## Kostenlose öffentliche Server +[**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases) + +[**Nächtliche Erstellung**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## Freie öffentliche Server Nachfolgend sind die Server gelistet, die du kostenlos nutzen kannst. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird. - -| Standort | Anbieter | Spezifikationen | Kommentar | -| --------- | ------------- | ------------------ | ---------- | -| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | | -| Germany | Codext | 2 vCPU / 4GB RAM | -| Germany | Hetzner | 4 vCPU / 8GB RAM | -| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | -| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| Standort | Anbieter | Spezifikation | +| --------- | ------------- | ------------------ | +| Südkorea (Seoul) | AWS lightsail | 1 vCPU / 0,5 GB RAM | +| Deutschland | Hetzner | 2 vCPU / 4 GB RAM | +| Deutschland | Codext | 4 vCPU / 8 GB RAM | +| Finnland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM | +| Ukraine (Kiew) | dc.volia (2VM) | 2 vCPU / 4 GB RAM | ## Abhängigkeiten -Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) oder Flutter für die GUI. Bitte lade die dynamische Sciter Bibliothek selbst herunter. +Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter. + +Bitte lade die dynamische Bibliothek Sciter selbst herunter. [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) +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) -## Die groben Schritte zum Kompilieren +## Grobe Schritte zum Kompilieren -- Bereite deine Rust Entwicklungsumgebung und C++ Build-Umgebung vor +- Bereite deine Rust-Entwicklungsumgebung und C++-Build-Umgebung vor -- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die `VCPKG_ROOT` Systemumgebungsvariable hinzu +- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die Systemumgebungsvariable `VCPKG_ROOT` hinzu - Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static` - - Linux/MacOS: `vcpkg install libvpx libyuv opus` + - Linux/macOS: `vcpkg install libvpx libyuv opus` - Nutze `cargo run` +## [Erstellen](https://rustdesk.com/docs/de/dev/build/) + ## Kompilieren auf Linux ### 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) ```sh @@ -82,7 +103,7 @@ export VCPKG_ROOT=$HOME/vcpkg vcpkg/vcpkg install libvpx libyuv opus ``` -### libvpx reparieren (Für Fedora) +### libvpx reparieren (für Fedora) ```sh cd vcpkg/buildtrees/libvpx/src @@ -105,16 +126,40 @@ 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 ``` -### Ändere Wayland zu X11 (Xorg) +### Wayland zu X11 (Xorg) ändern -RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard GNOME Session zu nutzen. +RustDesk unterstützt Wayland nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard-GNOME-Sitzung zu nutzen. + +## Wayland-Unterstützung + +Wayland scheint keine API für das Senden von Tastatureingaben an andere Fenster zu bieten. Daher verwendet RustDesk eine API von einer niedrigeren Ebene, nämlich dem Gerät `/dev/uinput` (Linux-Kernelebene). + +Wenn Wayland die kontrollierte Seite ist, müssen Sie wie folgt vorgehen: +```bash +# Dienst uinput starten +$ sudo rustdesk --service +$ rustdesk +``` +**Hinweis**: Die Wayland-Bildschirmaufnahme verwendet verschiedene Schnittstellen. RustDesk unterstützt derzeit nur org.freedesktop.portal.ScreenCast. +```bash +$ dbus-send --session --print-reply \ + --dest=org.freedesktop.portal.Desktop \ + /org/freedesktop/portal/desktop \ + org.freedesktop.DBus.Properties.Get \ + string:org.freedesktop.portal.ScreenCast string:version +# Keine Unterstützung +Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” +# Unterstützung +method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 + variant uint32 4 +``` ## Auf Docker kompilieren -Beginne damit, das Repository zu klonen und den Docker Container zu bauen: +Beginne damit, das Repository zu klonen und den Docker-Container zu bauen: ```sh git clone https://github.com/rustdesk/rustdesk @@ -122,13 +167,13 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -Jedes Mal, wenn du das Programm kompilieren musst, nutze diesen Befehl: +Führe jedes Mal, wenn du das Programm kompilieren musst, folgenden Befehl aus: ```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 ``` -Bedenke, dass das erste Mal Kompilieren länger dauern kann, da die Abhängigkeiten erst kompiliert werden müssen bevor sie zwischengespeichert werden können. Nachfolgende Kompiliervorgänge werden schneller sein. Falls du zusätzliche oder andere Argumente für den Kompilierbefehl angeben musst, kannst du diese am Ende des Befehls an der `` Position machen. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du das tun, indem du `--release` am Ende des Befehls anhängst. Das daraus entstehende Programm kannst du im “target” Ordner auf deinem System finden. Du kannst es mit folgenden Befehlen ausführen: +Bedenke, dass das erste Kompilieren länger dauern kann, bis die Abhängigkeiten zwischengespeichert sind. Nachfolgende Kompiliervorgänge sind schneller. Wenn du verschiedene Argumente für den Kompilierbefehl angeben musst, kannst du dies am Ende des Befehls an der Position `` tun. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du `--release` am Ende des Befehls anhängen. Das daraus entstehende Programm findest du im Zielordner auf deinem System. Du kannst es mit folgendem Befehl ausführen: ```sh target/debug/rustdesk @@ -140,18 +185,20 @@ Oder, wenn du eine Releaseversion benutzt: target/release/rustdesk ``` -Bitte stelle sicher, dass du diese Befehle vom Stammverzeichnis vom RustDesk Repository nutzt. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass Unterbefehle von Cargo, wie z. B. `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System. +Bitte stelle sicher, dass du diese Befehle im Stammverzeichnis des RustDesk-Repositorys nutzt. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass andere Cargo-Unterbefehle wie `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System. ## Dateistruktur -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer und ein paar andere nützliche Funktionen +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video-Codec, Konfiguration, TCP/UDP-Wrapper, Protokoll-Puffer, fs-Funktionen für Dateitransfer und ein paar andere nützliche Funktionen - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Bildschirmaufnahme -- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatur-Steuerung +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatursteuerung - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI -- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerk Verbindungen +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerkverbindungen - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Starten einer Peer-Verbindung -- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, für Verbindung von außen warten, direkt (TCP hole punching) oder weitergeleitet +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, warten auf direkte (TCP hole punching) oder weitergeleitete Verbindung - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter-Code für Handys +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript für Flutter-Webclient ## Screenshots From b733ad93796de81735a52068a78e89a2ef30c170 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 10:19:28 +0800 Subject: [PATCH 110/202] refact register_breakdown_handler Signed-off-by: fufesou --- Cargo.lock | 6 +- Cargo.toml | 2 - libs/enigo/Cargo.toml | 3 - libs/enigo/src/linux/xdo.rs | 4 +- libs/hbb_common/Cargo.toml | 2 + libs/hbb_common/src/lib.rs | 1 + libs/hbb_common/src/platform/mod.rs | 83 ++++++++++++++++++++++++++++ libs/scrap/Cargo.toml | 1 - libs/scrap/src/lib.rs | 2 +- libs/scrap/src/quartz/capturer.rs | 2 +- libs/scrap/src/quartz/config.rs | 2 +- libs/scrap/src/quartz/ffi.rs | 2 +- libs/scrap/src/x11/capturer.rs | 2 +- libs/scrap/src/x11/ffi.rs | 2 +- libs/scrap/src/x11/iter.rs | 2 +- src/client.rs | 2 +- src/core_main.rs | 4 +- src/platform/linux.rs | 85 +---------------------------- src/server/portable_service.rs | 2 +- 19 files changed, 101 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b308de149..eb26f2ed4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1566,7 +1566,6 @@ version = "0.0.14" dependencies = [ "core-graphics 0.22.3", "hbb_common", - "libc", "log", "objc", "pkg-config", @@ -2598,6 +2597,7 @@ name = "hbb_common" version = "0.1.0" dependencies = [ "anyhow", + "backtrace", "bytes", "chrono", "confy", @@ -2608,6 +2608,7 @@ dependencies = [ "futures", "futures-util", "lazy_static", + "libc", "log", "mac_address", "machine-uid", @@ -4813,7 +4814,6 @@ dependencies = [ "arboard", "async-process", "async-trait", - "backtrace", "base64", "bytes", "cc", @@ -4847,7 +4847,6 @@ dependencies = [ "include_dir", "jni 0.19.0", "lazy_static", - "libc", "libpulse-binding", "libpulse-simple-binding", "mac_address", @@ -5046,7 +5045,6 @@ dependencies = [ "hwcodec", "jni 0.19.0", "lazy_static", - "libc", "log", "ndk 0.7.0", "num_cpus", diff --git a/Cargo.toml b/Cargo.toml index 9588d10b6..0ebe49fdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ cfg-if = "1.0" lazy_static = "1.4" sha2 = "0.10" repng = "0.2" -libc = "0.2" parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" } flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] } runas = "0.2" @@ -121,7 +120,6 @@ mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } dbus = "0.9" dbus-crossroads = "0.5" -backtrace = "0.3" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index cc4173a97..fc4db9a63 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -37,8 +37,5 @@ core-graphics = "0.22" objc = "0.2" unicode-segmentation = "1.6" -[target.'cfg(target_os = "linux")'.dependencies] -libc = "0.2" - [build-dependencies] pkg-config = "0.3" diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs index 2115d7283..f0f7d49af 100644 --- a/libs/enigo/src/linux/xdo.rs +++ b/libs/enigo/src/linux/xdo.rs @@ -1,8 +1,6 @@ -use libc; - use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; -use self::libc::{c_char, c_int, c_void, useconds_t}; +use hbb_common::libc::{c_char, c_int, c_void, useconds_t}; use std::{borrow::Cow, ffi::CString, ptr}; const CURRENT_WINDOW: c_int = 0; diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 59f0896cc..e7a7eacd1 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -31,6 +31,8 @@ sodiumoxide = "0.2" regex = "1.4" tokio-socks = { git = "https://github.com/open-trade/tokio-socks" } chrono = "0.4" +backtrace = "0.3" +libc = "0.2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] mac_address = "1.1" diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 1c49adfb7..99cb6f408 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -39,6 +39,7 @@ pub use tokio_socks::IntoTargetAddr; pub use tokio_socks::TargetAddr; pub mod password_security; pub use chrono; +pub use libc; pub use directories_next; pub mod keyboard; diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index 8daba257f..05ecd292d 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -1,2 +1,85 @@ #[cfg(target_os = "linux")] pub mod linux; + +use crate::{log, config::Config, ResultType}; +use std::{collections::HashMap, process::{Command, exit}}; + +extern "C" fn breakdown_signal_handler(sig: i32) { + let mut stack = vec![]; + backtrace::trace(|frame| { + backtrace::resolve_frame(frame, |symbol| { + if let Some(name) = symbol.name() { + stack.push(name.to_string()); + } + }); + true // keep going to the next frame + }); + let mut info = String::default(); + if stack.iter().any(|s| { + s.contains(&"nouveau_pushbuf_kick") + || s.to_lowercase().contains("nvidia") + || s.contains("gdk_window_end_draw_frame") + }) { + Config::set_option("allow-always-software-render".to_string(), "Y".to_string()); + info = "Always use software rendering will be set.".to_string(); + log::info!("{}", info); + } + log::error!( + "Got signal {} and exit. stack:\n{}", + sig, + stack.join("\n").to_string() + ); + if !info.is_empty() { + system_message( + "RustDesk", + &format!("Got signal {} and exit.{}", sig, info), + true, + ) + .ok(); + } + exit(0); +} + +/// forever: may not work +pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> { + let cmds: HashMap<&str, Vec<&str>> = HashMap::from([ + ("notify-send", [title, msg].to_vec()), + ( + "zenity", + [ + "--info", + "--timeout", + if forever { "0" } else { "3" }, + "--title", + title, + "--text", + msg, + ] + .to_vec(), + ), + ("kdialog", ["--title", title, "--msgbox", msg].to_vec()), + ( + "xmessage", + [ + "-center", + "-timeout", + if forever { "0" } else { "3" }, + title, + msg, + ] + .to_vec(), + ), + ]); + for (k, v) in cmds { + if Command::new(k).args(v).spawn().is_ok() { + return Ok(()); + } + } + crate::bail!("failed to post system message"); +} + +pub fn register_breakdown_handler() { + unsafe { + libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); + } +} diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index e2eb43177..82cb88faf 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -16,7 +16,6 @@ mediacodec = ["ndk"] [dependencies] block = "0.1" cfg-if = "1.0" -libc = "0.2" num_cpus = "1.13" lazy_static = "1.4" hbb_common = { path = "../hbb_common" } diff --git a/libs/scrap/src/lib.rs b/libs/scrap/src/lib.rs index 504f0a4b3..77070d1a2 100644 --- a/libs/scrap/src/lib.rs +++ b/libs/scrap/src/lib.rs @@ -2,7 +2,7 @@ extern crate block; #[macro_use] extern crate cfg_if; -pub extern crate libc; +pub use hbb_common::libc; #[cfg(dxgi)] extern crate winapi; diff --git a/libs/scrap/src/quartz/capturer.rs b/libs/scrap/src/quartz/capturer.rs index 5be55ea22..cf442c2b4 100644 --- a/libs/scrap/src/quartz/capturer.rs +++ b/libs/scrap/src/quartz/capturer.rs @@ -1,7 +1,7 @@ use std::ptr; use block::{Block, ConcreteBlock}; -use libc::c_void; +use hbb_common::libc::c_void; use std::sync::{Arc, Mutex}; use super::config::Config; diff --git a/libs/scrap/src/quartz/config.rs b/libs/scrap/src/quartz/config.rs index 11a6d5fc0..d5f992f0b 100644 --- a/libs/scrap/src/quartz/config.rs +++ b/libs/scrap/src/quartz/config.rs @@ -1,6 +1,6 @@ use std::ptr; -use libc::c_void; +use hbb_common::libc::c_void; use super::ffi::*; diff --git a/libs/scrap/src/quartz/ffi.rs b/libs/scrap/src/quartz/ffi.rs index ca39c0a61..6b8c6e0e1 100644 --- a/libs/scrap/src/quartz/ffi.rs +++ b/libs/scrap/src/quartz/ffi.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use block::RcBlock; -use libc::c_void; +use hbb_common::libc::c_void; pub type CGDisplayStreamRef = *mut c_void; pub type CFDictionaryRef = *mut c_void; diff --git a/libs/scrap/src/x11/capturer.rs b/libs/scrap/src/x11/capturer.rs index 0dcfcfdab..6486af55c 100644 --- a/libs/scrap/src/x11/capturer.rs +++ b/libs/scrap/src/x11/capturer.rs @@ -1,6 +1,6 @@ use std::{io, ptr, slice}; -use libc; +use hbb_common::libc; use super::ffi::*; use super::Display; diff --git a/libs/scrap/src/x11/ffi.rs b/libs/scrap/src/x11/ffi.rs index 5df5c46a8..500f57615 100644 --- a/libs/scrap/src/x11/ffi.rs +++ b/libs/scrap/src/x11/ffi.rs @@ -1,6 +1,6 @@ #![allow(non_camel_case_types)] -use libc::c_void; +use hbb_common::libc::c_void; #[link(name = "xcb")] #[link(name = "xcb-shm")] diff --git a/libs/scrap/src/x11/iter.rs b/libs/scrap/src/x11/iter.rs index cb3310be9..406c27352 100644 --- a/libs/scrap/src/x11/iter.rs +++ b/libs/scrap/src/x11/iter.rs @@ -1,7 +1,7 @@ use std::ptr; use std::rc::Rc; -use libc; +use hbb_common::libc; use super::ffi::*; use super::{Display, Rect, Server}; diff --git a/src/client.rs b/src/client.rs index 51e7f9a29..8683dad1f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -101,7 +101,7 @@ pub fn get_key_state(key: enigo::Key) -> bool { cfg_if::cfg_if! { if #[cfg(target_os = "android")] { -use libc::{c_float, c_int, c_void}; +use hbb_common::libc::{c_float, c_int, c_void}; type Oboe = *mut c_void; extern "C" { fn create_oboe_player(channels: c_int, sample_rate: c_int) -> Oboe; diff --git a/src/core_main.rs b/src/core_main.rs index e2f3f80e0..7d722e6c5 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,4 +1,4 @@ -use hbb_common::log; +use hbb_common::{log, platform::register_breakdown_handler}; /// shared by flutter and sciter main function /// @@ -38,10 +38,10 @@ pub fn core_main() -> Option> { } i += 1; } + register_breakdown_handler(); #[cfg(target_os = "linux")] #[cfg(feature = "flutter")] { - crate::platform::linux::register_breakdown_handler(); let (k, v) = ("LIBGL_ALWAYS_SOFTWARE", "true"); if !hbb_common::config::Config::get_option("allow-always-software-render").is_empty() { std::env::set_var(k, v); diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 8fa95ac90..2ff2d3729 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,7 +1,7 @@ use super::{CursorData, ResultType}; pub use hbb_common::platform::linux::*; use hbb_common::{allow_err, bail, log}; -use libc::{c_char, c_int, c_void}; +use hbb_common::libc::{c_char, c_int, c_void}; use std::{ cell::RefCell, collections::HashMap, @@ -642,86 +642,3 @@ pub fn get_double_click_time() -> u32 { double_click_time } } - -/// forever: may not work -pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> { - let cmds: HashMap<&str, Vec<&str>> = HashMap::from([ - ("notify-send", [title, msg].to_vec()), - ( - "zenity", - [ - "--info", - "--timeout", - if forever { "0" } else { "3" }, - "--title", - title, - "--text", - msg, - ] - .to_vec(), - ), - ("kdialog", ["--title", title, "--msgbox", msg].to_vec()), - ( - "xmessage", - [ - "-center", - "-timeout", - if forever { "0" } else { "3" }, - title, - msg, - ] - .to_vec(), - ), - ]); - for (k, v) in cmds { - if std::process::Command::new(k).args(v).spawn().is_ok() { - return Ok(()); - } - } - bail!("failed to post system message"); -} - -extern "C" fn breakdown_signal_handler(sig: i32) { - let mut stack = vec![]; - backtrace::trace(|frame| { - backtrace::resolve_frame(frame, |symbol| { - if let Some(name) = symbol.name() { - stack.push(name.to_string()); - } - }); - true // keep going to the next frame - }); - let mut info = String::default(); - if stack.iter().any(|s| { - s.contains(&"nouveau_pushbuf_kick") - || s.to_lowercase().contains("nvidia") - || s.contains("gdk_window_end_draw_frame") - }) { - hbb_common::config::Config::set_option( - "allow-always-software-render".to_string(), - "Y".to_string(), - ); - info = "Always use software rendering will be set.".to_string(); - log::info!("{}", info); - } - log::error!( - "Got signal {} and exit. stack:\n{}", - sig, - stack.join("\n").to_string() - ); - if !info.is_empty() { - system_message( - "RustDesk", - &format!("Got signal {} and exit.{}", sig, info), - true, - ) - .ok(); - } - std::process::exit(0); -} - -pub fn register_breakdown_handler() { - unsafe { - libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); - } -} diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index c783fef52..fd17fd469 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -492,7 +492,7 @@ pub mod client { let mut option = SHMEM.lock().unwrap(); let shmem = option.as_mut().unwrap(); unsafe { - libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); + hbb_common::libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); } drop(option); match para { From a333a261fdfe636f4bd9830dc25a803f124d63b3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 11:40:59 +0800 Subject: [PATCH 111/202] add alert for macos Signed-off-by: fufesou --- Cargo.lock | 12 +++++ libs/hbb_common/Cargo.toml | 3 ++ libs/hbb_common/examples/system_message.rs | 15 ++++++ libs/hbb_common/src/platform/linux.rs | 40 +++++++++++++++ libs/hbb_common/src/platform/macos.rs | 55 +++++++++++++++++++++ libs/hbb_common/src/platform/mod.rs | 57 ++++++---------------- src/core_main.rs | 5 +- 7 files changed, 145 insertions(+), 42 deletions(-) create mode 100644 libs/hbb_common/examples/system_message.rs create mode 100644 libs/hbb_common/src/platform/macos.rs diff --git a/Cargo.lock b/Cargo.lock index eb26f2ed4..48981e169 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2612,6 +2612,7 @@ dependencies = [ "log", "mac_address", "machine-uid", + "osascript", "protobuf", "protobuf-codegen", "quinn", @@ -3926,6 +3927,17 @@ version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +[[package]] +name = "osascript" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc" +dependencies = [ + "serde 1.0.149", + "serde_derive", + "serde_json 1.0.89", +] + [[package]] name = "pango" version = "0.16.5" diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index e7a7eacd1..0457bb19a 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -48,6 +48,9 @@ protobuf-codegen = { version = "3.1" } [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["winuser"] } +[target.'cfg(target_os = "macos")'.dependencies] +osascript = "0.3.0" + [dev-dependencies] toml = "0.5" serde_json = "1.0" diff --git a/libs/hbb_common/examples/system_message.rs b/libs/hbb_common/examples/system_message.rs new file mode 100644 index 000000000..26320e329 --- /dev/null +++ b/libs/hbb_common/examples/system_message.rs @@ -0,0 +1,15 @@ +extern crate hbb_common; + +fn main() { + #[cfg(target_os = "linux")] + linux::system_message("test title", "test message", true).ok(); + #[cfg(target_os = "macos")] + macos::alert( + "RustDesk".to_owned(), + "critical".to_owned(), + "test title".to_owned(), + "test message".to_owned(), + ["Ok".to_owned()].to_vec(), + ) + .ok(); +} diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 7c107d11c..191ea2e6f 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -1,4 +1,5 @@ use crate::ResultType; +use std::{collections::HashMap, process::Command}; lazy_static::lazy_static! { pub static ref DISTRO: Distro = Distro::new(); @@ -155,3 +156,42 @@ fn run_loginctl(args: Option>) -> std::io::Result ResultType<()> { + let cmds: HashMap<&str, Vec<&str>> = HashMap::from([ + ("notify-send", [title, msg].to_vec()), + ( + "zenity", + [ + "--info", + "--timeout", + if forever { "0" } else { "3" }, + "--title", + title, + "--text", + msg, + ] + .to_vec(), + ), + ("kdialog", ["--title", title, "--msgbox", msg].to_vec()), + ( + "xmessage", + [ + "-center", + "-timeout", + if forever { "0" } else { "3" }, + title, + msg, + ] + .to_vec(), + ), + ]); + for (k, v) in cmds { + if Command::new(k).args(v).spawn().is_ok() { + return Ok(()); + } + } + crate::bail!("failed to post system message"); +} diff --git a/libs/hbb_common/src/platform/macos.rs b/libs/hbb_common/src/platform/macos.rs new file mode 100644 index 000000000..299a21f93 --- /dev/null +++ b/libs/hbb_common/src/platform/macos.rs @@ -0,0 +1,55 @@ +use osascript; +use serde_derive; + +#[derive(Serialize)] +struct AlertParams { + title: String, + message: String, + alert_type: String, + buttons: Vec, +} + +#[derive(Deserialize)] +struct AlertResult { + #[serde(rename = "buttonReturned")] + button: String, +} + +/// Alert dialog, return the clicked button value. +/// +/// # Arguments +/// +/// * `app` - The app to execute the script. +/// * `alert_type` - Alert type. critical +/// * `title` - The alert title. +/// * `message` - The alert message. +/// * `buttons` - The buttons to show. +pub fn alert( + app: &str, + alert_type: &str, + title: &str, + message: String, + buttons: Vec, +) -> ResultType { + let script = osascript::JavaScript::new(format!( + " + var App = Application('{}'); + App.includeStandardAdditions = true; + return App.displayAlert($params.title, { + message: $params.message, + 'as': $params.alert_type, + buttons: $params.buttons, + }); + ", + app + )); + + script + .execute_with_params(AlertParams { + title, + message, + alert_type, + buttons, + })? + .button +} diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index 05ecd292d..89a3a1569 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -1,8 +1,11 @@ #[cfg(target_os = "linux")] pub mod linux; -use crate::{log, config::Config, ResultType}; -use std::{collections::HashMap, process::{Command, exit}}; +#[cfg(target_os = "macos")] +pub mod macos; + +use crate::{config::Config, log}; +use std::process::exit; extern "C" fn breakdown_signal_handler(sig: i32) { let mut stack = vec![]; @@ -30,54 +33,26 @@ extern "C" fn breakdown_signal_handler(sig: i32) { stack.join("\n").to_string() ); if !info.is_empty() { - system_message( + #[cfg(target_os = "linux")] + linux::system_message( "RustDesk", &format!("Got signal {} and exit.{}", sig, info), true, ) .ok(); + #[cfg(target_os = "macos")] + macos::alert( + "RustDesk".to_owned(), + "critical".to_owned(), + "Crashed".to_owned(), + format!("Got signal {} and exit.{}", sig, info), + ["Ok".to_owned()].to_vec(), + ) + .ok(); } exit(0); } -/// forever: may not work -pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> { - let cmds: HashMap<&str, Vec<&str>> = HashMap::from([ - ("notify-send", [title, msg].to_vec()), - ( - "zenity", - [ - "--info", - "--timeout", - if forever { "0" } else { "3" }, - "--title", - title, - "--text", - msg, - ] - .to_vec(), - ), - ("kdialog", ["--title", title, "--msgbox", msg].to_vec()), - ( - "xmessage", - [ - "-center", - "-timeout", - if forever { "0" } else { "3" }, - title, - msg, - ] - .to_vec(), - ), - ]); - for (k, v) in cmds { - if Command::new(k).args(v).spawn().is_ok() { - return Ok(()); - } - } - crate::bail!("failed to post system message"); -} - pub fn register_breakdown_handler() { unsafe { libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); diff --git a/src/core_main.rs b/src/core_main.rs index 7d722e6c5..2619a1c07 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,4 +1,6 @@ -use hbb_common::{log, platform::register_breakdown_handler}; +use hbb_common::log; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::platform::register_breakdown_handler; /// shared by flutter and sciter main function /// @@ -38,6 +40,7 @@ pub fn core_main() -> Option> { } i += 1; } + #[cfg(not(any(target_os = "android", target_os = "ios")))] register_breakdown_handler(); #[cfg(target_os = "linux")] #[cfg(feature = "flutter")] From 626fdefb18ede90d7aa65511feaae4dd5630543d Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 12:01:46 +0800 Subject: [PATCH 112/202] debug macos and linux Signed-off-by: fufesou --- libs/hbb_common/examples/system_message.rs | 6 +++- libs/hbb_common/src/platform/macos.rs | 32 +++++++++++----------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/libs/hbb_common/examples/system_message.rs b/libs/hbb_common/examples/system_message.rs index 26320e329..347bec47f 100644 --- a/libs/hbb_common/examples/system_message.rs +++ b/libs/hbb_common/examples/system_message.rs @@ -1,4 +1,8 @@ extern crate hbb_common; +#[cfg(target_os = "linux")] +use hbb_common::platform::linux; +#[cfg(target_os = "macos")] +use hbb_common::platform::macos; fn main() { #[cfg(target_os = "linux")] @@ -6,7 +10,7 @@ fn main() { #[cfg(target_os = "macos")] macos::alert( "RustDesk".to_owned(), - "critical".to_owned(), + "warning".to_owned(), "test title".to_owned(), "test message".to_owned(), ["Ok".to_owned()].to_vec(), diff --git a/libs/hbb_common/src/platform/macos.rs b/libs/hbb_common/src/platform/macos.rs index 299a21f93..0008c6266 100644 --- a/libs/hbb_common/src/platform/macos.rs +++ b/libs/hbb_common/src/platform/macos.rs @@ -1,5 +1,6 @@ +use crate::ResultType; use osascript; -use serde_derive; +use serde_derive::{Deserialize, Serialize}; #[derive(Serialize)] struct AlertParams { @@ -20,36 +21,35 @@ struct AlertResult { /// # Arguments /// /// * `app` - The app to execute the script. -/// * `alert_type` - Alert type. critical +/// * `alert_type` - Alert type. . informational, warning, critical /// * `title` - The alert title. /// * `message` - The alert message. /// * `buttons` - The buttons to show. pub fn alert( - app: &str, - alert_type: &str, - title: &str, + app: String, + alert_type: String, + title: String, message: String, buttons: Vec, ) -> ResultType { - let script = osascript::JavaScript::new(format!( + let script = osascript::JavaScript::new(&format!( " var App = Application('{}'); App.includeStandardAdditions = true; - return App.displayAlert($params.title, { + return App.displayAlert($params.title, {{ message: $params.message, 'as': $params.alert_type, buttons: $params.buttons, - }); + }}); ", app )); - script - .execute_with_params(AlertParams { - title, - message, - alert_type, - buttons, - })? - .button + let result: AlertResult = script.execute_with_params(AlertParams { + title, + message, + alert_type, + buttons, + })?; + Ok(result.button) } From 8852d97efc3f119ee299447e4f23f40baf7ba7a7 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 12:52:41 +0800 Subject: [PATCH 113/202] fix build linux Signed-off-by: fufesou --- src/platform/linux.rs | 9 ++++----- src/server/portable_service.rs | 4 ++-- src/tray.rs | 4 ++-- src/ui_interface.rs | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 2ff2d3729..32c32efb9 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,10 +1,9 @@ use super::{CursorData, ResultType}; +use hbb_common::libc::{c_char, c_int, c_long, c_void}; pub use hbb_common::platform::linux::*; use hbb_common::{allow_err, bail, log}; -use hbb_common::libc::{c_char, c_int, c_void}; use std::{ cell::RefCell, - collections::HashMap, path::PathBuf, sync::{ atomic::{AtomicBool, Ordering}, @@ -54,8 +53,8 @@ pub struct xcb_xfixes_get_cursor_image { pub height: u16, pub xhot: u16, pub yhot: u16, - pub cursor_serial: libc::c_long, - pub pixels: *const libc::c_long, + pub cursor_serial: c_long, + pub pixels: *const c_long, } pub fn get_cursor_pos() -> Option<(i32, i32)> { @@ -637,7 +636,7 @@ pub fn get_double_click_time() -> u32 { settings, property.as_ptr(), &mut double_click_time as *mut u32, - 0 as *const libc::c_void, + 0 as *const c_void, ); double_click_time } diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index fd17fd469..7514ead38 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -2,7 +2,7 @@ use core::slice; use hbb_common::{ allow_err, anyhow::anyhow, - bail, log, + bail, libc, log, message_proto::{KeyEvent, MouseEvent}, protobuf::Message, tokio::{self, sync::mpsc}, @@ -492,7 +492,7 @@ pub mod client { let mut option = SHMEM.lock().unwrap(); let shmem = option.as_mut().unwrap(); unsafe { - hbb_common::libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); + libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); } drop(option); match para { diff --git a/src/tray.rs b/src/tray.rs index b449bbbd3..12523605d 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,4 +1,4 @@ -#[cfg(any(target_os = "windows"))] +#[cfg(target_os = "windows")] use super::ui_interface::get_option_opt; #[cfg(target_os = "windows")] use std::sync::{Arc, Mutex}; @@ -80,7 +80,7 @@ pub fn start_tray() { /// Check if service is stoped. /// Return [`true`] if service is stoped, [`false`] otherwise. #[inline] -#[cfg(any(target_os = "windows"))] +#[cfg(target_os = "windows")] fn is_service_stopped() -> bool { if let Some(v) = get_option_opt("stop-service") { v == "Y" diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 26038218e..f44bb4eea 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -128,7 +128,7 @@ pub fn get_license() -> String { } #[inline] -#[cfg(any(target_os = "linux", target_os = "windows"))] +#[cfg(target_os = "windows")] pub fn get_option_opt(key: &str) -> Option { OPTIONS.lock().unwrap().get(key).map(|x| x.clone()) } From 5f0d7a0c08e7f0a8f6b1c5518568ebb8252484bb Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Sun, 19 Feb 2023 12:18:58 +0530 Subject: [PATCH 114/202] devcontainer --- .devcontainer/Dockerfile | 22 ++++++++++++---------- .devcontainer/devcontainer.json | 11 ++++++++--- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 86c11ccf6..92eb7a9fc 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,15 +1,20 @@ -FROM mcr.microsoft.com/devcontainers/base:ubuntu -WORKDIR / -RUN apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +FROM mcr.microsoft.com/devcontainers/base:ubuntu-22.04 +ENV HOME=/home/vscode +ENV WORKDIR=$HOME/rustdesk -RUN git clone https://github.com/microsoft/vcpkg && cd vcpkg && git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82 +WORKDIR $HOME +RUN sudo apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +WORKDIR / + +RUN git clone https://github.com/microsoft/vcpkg +WORKDIR vcpkg +RUN git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82 RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics RUN /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus -RUN groupadd -r user && useradd -r -g user user --home /home/user && mkdir -p /home/user && chown user /home/user && echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user -WORKDIR /home/user +USER vscode +WORKDIR $HOME RUN wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so -USER user RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh RUN chmod +x rustup.sh RUN ./rustup.sh -y @@ -18,6 +23,3 @@ RUN ./rustup.sh -y RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.3-stable.tar.xz RUN tar xf flutter_linux_3.7.3-stable.tar.xz RUN export PATH="$PATH:/home/user/flutter/bin" - -USER root -ENV HOME=/home/user diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 426127fd9..432d05136 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,10 +4,15 @@ "dockerfile": "./Dockerfile", "context": "." }, - "workspaceMount": "source=${localWorkspaceFolder},target=/home/user/rustdesk,type=bind,consistency=cache", - "workspaceFolder": "/home/user/rustdesk", + "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache", + "workspaceFolder": "/home/vscode/rustdesk", "postStartCommand": "./entrypoint", - "remoteUser": "user", + "features": { + "ghcr.io/devcontainers/features/java:1": {}, + "ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": { + "PACKAGES": "platform-tools,ndk;22.1.7171670" + } + }, "customizations": { "vscode": { "extensions": [ From 48a0d25e7303c81952087ee5e1b512eda9ea323c Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Sun, 19 Feb 2023 13:04:58 +0000 Subject: [PATCH 115/202] dockerfile --- .devcontainer/Dockerfile | 23 ++++++++++++++++++++--- flutter/pubspec.lock | 2 +- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 92eb7a9fc..6b86e88d2 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -10,16 +10,33 @@ RUN git clone https://github.com/microsoft/vcpkg WORKDIR vcpkg RUN git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82 RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics -RUN /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus +ENV VCPKG_ROOT=/vcpkg +RUN $VCPKG_ROOT/vcpkg --disable-metrics install libvpx libyuv opus + +WORKDIR / +RUN wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz && tar xzf dep.tar.gz + USER vscode WORKDIR $HOME RUN wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh RUN chmod +x rustup.sh -RUN ./rustup.sh -y +RUN $HOME/rustup.sh -y +RUN $HOME/.cargo/bin/rustup target add aarch64-linux-android +RUN $HOME/.cargo/bin/cargo install cargo-ndk # Install Flutter RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.3-stable.tar.xz RUN tar xf flutter_linux_3.7.3-stable.tar.xz -RUN export PATH="$PATH:/home/user/flutter/bin" +ENV PATH="$PATH:$HOME/flutter/bin" +RUN dart pub global activate ffigen 5.0.1 + + +# Install packages +RUN sudo apt-get install -y libclang-dev +RUN sudo apt install -y gcc-multilib + +WORKDIR $WORKDIR +ENV ANDROID_NDK_HOME=/opt/android/ndk/22.1.7171670 +# Somehow try to automate flutter pub get \ No newline at end of file diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index cd618dfc4..0a1b1dcc8 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1499,5 +1499,5 @@ packages: source: hosted version: "0.1.1" sdks: - dart: ">=2.18.0 <4.0.0" + dart: ">=2.18.0 <3.0.0" flutter: ">=3.3.0" From e1254c0b2415baaf8ea5be7b2fd38b8c12d93f0a Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 21:11:17 +0800 Subject: [PATCH 116/202] macos better alert Signed-off-by: fufesou --- libs/hbb_common/examples/system_message.rs | 11 +++++----- libs/hbb_common/src/platform/mod.rs | 25 +++++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/libs/hbb_common/examples/system_message.rs b/libs/hbb_common/examples/system_message.rs index 347bec47f..0be788428 100644 --- a/libs/hbb_common/examples/system_message.rs +++ b/libs/hbb_common/examples/system_message.rs @@ -6,14 +6,15 @@ use hbb_common::platform::macos; fn main() { #[cfg(target_os = "linux")] - linux::system_message("test title", "test message", true).ok(); + let res = linux::system_message("test title", "test message", true); #[cfg(target_os = "macos")] - macos::alert( - "RustDesk".to_owned(), + let res = macos::alert( + "System Preferences".to_owned(), "warning".to_owned(), "test title".to_owned(), "test message".to_owned(), ["Ok".to_owned()].to_vec(), - ) - .ok(); + ); + #[cfg(any(target_os = "linux", target_os = "macos"))] + println!("result {:?}", &res); } diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index 89a3a1569..0a4299ae2 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -41,14 +41,23 @@ extern "C" fn breakdown_signal_handler(sig: i32) { ) .ok(); #[cfg(target_os = "macos")] - macos::alert( - "RustDesk".to_owned(), - "critical".to_owned(), - "Crashed".to_owned(), - format!("Got signal {} and exit.{}", sig, info), - ["Ok".to_owned()].to_vec(), - ) - .ok(); + { + use std::sync::mpsc::channel; + use std::time::Duration; + let (tx, rx) = channel(); + std::thread::spawn(move || { + macos::alert( + "System Preferences".to_owned(), + "critical".to_owned(), + "RustDesk Crashed".to_owned(), + format!("Got signal {} and exit.{}", sig, info), + ["Ok".to_owned()].to_vec(), + ) + .ok(); + let _ = tx.send(()); + }); + let _ = rx.recv_timeout(Duration::from_millis(1_000)); + } } exit(0); } From b4beb78e8f6ce185807581bc5e40f6c50c4f837d Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 21:28:48 +0800 Subject: [PATCH 117/202] macOS, ignore alert for now Signed-off-by: fufesou --- libs/hbb_common/src/platform/mod.rs | 37 +++++++++++++++-------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index 0a4299ae2..b65980c1a 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -40,24 +40,25 @@ extern "C" fn breakdown_signal_handler(sig: i32) { true, ) .ok(); - #[cfg(target_os = "macos")] - { - use std::sync::mpsc::channel; - use std::time::Duration; - let (tx, rx) = channel(); - std::thread::spawn(move || { - macos::alert( - "System Preferences".to_owned(), - "critical".to_owned(), - "RustDesk Crashed".to_owned(), - format!("Got signal {} and exit.{}", sig, info), - ["Ok".to_owned()].to_vec(), - ) - .ok(); - let _ = tx.send(()); - }); - let _ = rx.recv_timeout(Duration::from_millis(1_000)); - } + // Ignore alert info for now. + // #[cfg(target_os = "macos")] + // { + // use std::sync::mpsc::channel; + // use std::time::Duration; + // let (tx, rx) = channel(); + // std::thread::spawn(move || { + // macos::alert( + // "System Preferences".to_owned(), + // "critical".to_owned(), + // "RustDesk Crashed".to_owned(), + // format!("Got signal {} and exit.{}", sig, info), + // ["Ok".to_owned()].to_vec(), + // ) + // .ok(); + // let _ = tx.send(()); + // }); + // let _ = rx.recv_timeout(Duration::from_millis(1_000)); + // } } exit(0); } From 0491950e012f9d3ac86601126e21ee346eb1439a Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 19 Feb 2023 22:29:10 +0800 Subject: [PATCH 118/202] macos remove unused code Signed-off-by: fufesou --- libs/hbb_common/src/platform/macos.rs | 2 +- libs/hbb_common/src/platform/mod.rs | 19 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/libs/hbb_common/src/platform/macos.rs b/libs/hbb_common/src/platform/macos.rs index 0008c6266..dd83a8738 100644 --- a/libs/hbb_common/src/platform/macos.rs +++ b/libs/hbb_common/src/platform/macos.rs @@ -16,7 +16,7 @@ struct AlertResult { button: String, } -/// Alert dialog, return the clicked button value. +/// Firstly run the specified app, then alert a dialog. Return the clicked button value. /// /// # Arguments /// diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index b65980c1a..aa929ca99 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -40,25 +40,6 @@ extern "C" fn breakdown_signal_handler(sig: i32) { true, ) .ok(); - // Ignore alert info for now. - // #[cfg(target_os = "macos")] - // { - // use std::sync::mpsc::channel; - // use std::time::Duration; - // let (tx, rx) = channel(); - // std::thread::spawn(move || { - // macos::alert( - // "System Preferences".to_owned(), - // "critical".to_owned(), - // "RustDesk Crashed".to_owned(), - // format!("Got signal {} and exit.{}", sig, info), - // ["Ok".to_owned()].to_vec(), - // ) - // .ok(); - // let _ = tx.send(()); - // }); - // let _ = rx.recv_timeout(Duration::from_millis(1_000)); - // } } exit(0); } From c2fa74dbbc5ed3cbf0c222876d5ce91525d7f20c Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sun, 19 Feb 2023 22:30:58 +0800 Subject: [PATCH 119/202] Update mod.rs --- libs/hbb_common/src/platform/mod.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index b65980c1a..aa929ca99 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -40,25 +40,6 @@ extern "C" fn breakdown_signal_handler(sig: i32) { true, ) .ok(); - // Ignore alert info for now. - // #[cfg(target_os = "macos")] - // { - // use std::sync::mpsc::channel; - // use std::time::Duration; - // let (tx, rx) = channel(); - // std::thread::spawn(move || { - // macos::alert( - // "System Preferences".to_owned(), - // "critical".to_owned(), - // "RustDesk Crashed".to_owned(), - // format!("Got signal {} and exit.{}", sig, info), - // ["Ok".to_owned()].to_vec(), - // ) - // .ok(); - // let _ = tx.send(()); - // }); - // let _ = rx.recv_timeout(Duration::from_millis(1_000)); - // } } exit(0); } From 0d321918d4cbe22924d2378005de1ab112ccadc3 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Sun, 19 Feb 2023 15:47:52 +0100 Subject: [PATCH 120/202] improve input of change ID --- flutter/lib/common/widgets/dialog.dart | 93 ++++++++++++++++++++++++-- src/lang/ca.rs | 6 +- src/lang/cn.rs | 6 +- src/lang/cs.rs | 6 +- src/lang/da.rs | 6 +- src/lang/de.rs | 6 +- src/lang/eo.rs | 6 +- src/lang/es.rs | 6 +- src/lang/fa.rs | 6 +- src/lang/fr.rs | 6 +- src/lang/gr.rs | 6 +- src/lang/hu.rs | 6 +- src/lang/id.rs | 6 +- src/lang/it.rs | 6 +- src/lang/ja.rs | 6 +- src/lang/ko.rs | 6 +- src/lang/kz.rs | 6 +- src/lang/nl.rs | 6 +- src/lang/pl.rs | 6 +- src/lang/pt_PT.rs | 6 +- src/lang/ptbr.rs | 6 +- src/lang/ro.rs | 6 +- src/lang/ru.rs | 6 +- src/lang/sk.rs | 6 +- src/lang/sl.rs | 6 +- src/lang/sq.rs | 6 +- src/lang/sr.rs | 6 +- src/lang/sv.rs | 6 +- src/lang/template.rs | 6 +- src/lang/th.rs | 6 +- src/lang/tr.rs | 6 +- src/lang/tw.rs | 6 +- src/lang/ua.rs | 6 +- src/lang/vn.rs | 6 +- 34 files changed, 254 insertions(+), 37 deletions(-) diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index e96a2b406..cdce6f12a 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -1,18 +1,74 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import '../../common.dart'; import '../../models/platform_model.dart'; +abstract class ValidationRule { + String get name; + bool validate(String value); +} + +class LengthRangeValidationRule extends ValidationRule { + final int _min; + final int _max; + + LengthRangeValidationRule(this._min, this._max); + + @override + String get name => translate('length %min% to %max%') + .replaceAll('%min%', _min.toString()) + .replaceAll('%max%', _max.toString()); + + @override + bool validate(String value) { + return value.length >= _min && value.length <= _max; + } +} + +class RegexValidationRule extends ValidationRule { + final String _name; + final RegExp _regex; + + RegexValidationRule(this._name, this._regex); + + @override + String get name => translate(_name); + + @override + bool validate(String value) { + return value.isNotEmpty ? value.contains(_regex) : false; + } +} + void changeIdDialog() { var newId = ""; var msg = ""; var isInProgress = false; TextEditingController controller = TextEditingController(); + final RxString rxId = controller.text.trim().obs; + + final rules = [ + RegexValidationRule('starts with a letter', RegExp(r'^[a-zA-Z]')), + LengthRangeValidationRule(6, 16), + RegexValidationRule('allowed characters', RegExp(r'^\w*$')) + ]; + gFFI.dialogManager.show((setState, close) { submit() async { debugPrint("onSubmit"); newId = controller.text.trim(); + + final Iterable violations = rules.where((r) => !r.validate(newId)); + if (violations.isNotEmpty) { + setState(() { + msg = + '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}'; + }); + return; + } + setState(() { msg = ""; isInProgress = true; @@ -31,7 +87,7 @@ void changeIdDialog() { } setState(() { isInProgress = false; - msg = translate(status); + msg = '${translate('Prompt')}: ${translate(status)}'; }); } @@ -46,18 +102,47 @@ void changeIdDialog() { ), TextField( decoration: InputDecoration( + labelText: translate('Your new ID'), border: const OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg)), + errorText: msg.isEmpty ? null : translate(msg), + suffixText: '${rxId.value.length}/16', + suffixStyle: const TextStyle(fontSize: 12, color: Colors.grey)), inputFormatters: [ LengthLimitingTextInputFormatter(16), // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) ], - maxLength: 16, controller: controller, autofocus: true, + onChanged: (value) { + setState(() { + rxId.value = value.trim(); + msg = ''; + }); + }, ), const SizedBox( - height: 4.0, + height: 8.0, + ), + Obx(() => Wrap( + runSpacing: 8, + spacing: 4, + children: rules.map((e) { + var checked = e.validate(rxId.value); + return Chip( + label: Text( + e.name, + style: TextStyle( + color: checked + ? const Color(0xFF0A9471) + : Color.fromARGB(255, 198, 86, 157)), + ), + backgroundColor: checked + ? const Color(0xFFD0F7ED) + : Color.fromARGB(255, 247, 205, 232)); + }).toList(), + )), + const SizedBox( + height: 8.0, ), Offstage( offstage: !isInProgress, child: const LinearProgressIndicator()) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 3220c824a..0d1eeff13 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "El portapapers està buit"), ("Stop service", "Aturar servei"), ("Change ID", "Canviar ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Només pots utilitzar caràcters a-z, A-Z, 0-9 e _ (guionet baix). El primer caràcter ha de ser a-z o A-Z. La longitut ha d'estar entre 6 i 16 caràcters."), ("Website", "Lloc web"), ("About", "Sobre"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servidor API"), ("invalid_http", "ha de començar amb http:// o https://"), ("Invalid IP", "IP incorrecta"), - ("id_change_tip", "Només pots utilitzar caràcters a-z, A-Z, 0-9 e _ (guionet baix). El primer caràcter ha de ser a-z o A-Z. La longitut ha d'estar entre 6 i 16 caràcters."), ("Invalid format", "Format incorrecte"), ("server_not_support", "Encara no és compatible amb el servidor"), ("Not available", "No disponible"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index d0fdcb3fd..63b59e8f1 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "拷贝配置信息到剪贴板后点击此按钮,可以自动导入配置"), ("Stop service", "停止服务"), ("Change ID", "改变ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "只可以使用字母a-z, A-Z, 0-9, _ (下划线)。首字母必须是a-z, A-Z。长度在6与16之间。"), ("Website", "网站"), ("About", "关于"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API服务器"), ("invalid_http", "必须以http://或者https://开头"), ("Invalid IP", "无效IP"), - ("id_change_tip", "只可以使用字母a-z, A-Z, 0-9, _ (下划线)。首字母必须是a-z, A-Z。长度在6与16之间。"), ("Invalid format", "无效格式"), ("server_not_support", "服务器暂不支持"), ("Not available", "已被占用"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index aca4778e6..f4d63cba9 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Schránka je prázdná"), ("Stop service", "Zastavit službu"), ("Change ID", "Změnit identifikátor"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Použít je mozné pouze znaky a-z, A-Z, 0-9 a _ (podtržítko). Dále je třeba aby začínalo na písmeno a-z, A-Z. Délka mezi 6 a 16 znaky."), ("Website", "Webové stránky"), ("About", "O aplikaci"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Server s API rozhraním"), ("invalid_http", "Je třeba, aby začínalo na http:// nebo https://"), ("Invalid IP", "Neplatná IP adresa"), - ("id_change_tip", "Použít je mozné pouze znaky a-z, A-Z, 0-9 a _ (podtržítko). Dále je třeba aby začínalo na písmeno a-z, A-Z. Délka mezi 6 a 16 znaky."), ("Invalid format", "Neplatný formát"), ("server_not_support", "Server zatím nepodporuje"), ("Not available", "Není k dispozici"), diff --git a/src/lang/da.rs b/src/lang/da.rs index 7b959a778..b3bf02dd2 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Udklipsholderen er tom"), ("Stop service", "Sluk for forbindelsesserveren"), ("Change ID", "Ændre ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Længde mellem 6 og 16."), ("Website", "Hjemmeside"), ("About", "Omkring"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Server"), ("invalid_http", "Skal begynde med http:// eller https://"), ("Invalid IP", "Ugyldig IP-adresse"), - ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Længde mellem 6 og 16."), ("Invalid format", "Ugyldigt format"), ("server_not_support", "Endnu ikke understøttet af serveren"), ("Not available", "ikke Tilgængelig"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 38f4fddab..ddc347605 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Zwischenablage ist leer"), ("Stop service", "Vermittlungsdienst stoppen"), ("Change ID", "ID ändern"), + ("Your new ID", "Ihre neue ID"), + ("length %min% to %max%", "Länge %min% bis %max%"), + ("starts with a letter", "Beginnt mit Buchstabe"), + ("allowed characters", "Erlaubte Zeichen"), + ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9 und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein und die Länge zwischen 6 und 16 Zeichen betragen."), ("Website", "Webseite"), ("About", "Über"), ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API-Server"), ("invalid_http", "Muss mit http:// oder https:// beginnen"), ("Invalid IP", "Ungültige IP-Adresse"), - ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9 und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein und die Länge zwischen 6 und 16 Zeichen betragen."), ("Invalid format", "Ungültiges Format"), ("server_not_support", "Diese Funktion wird noch nicht vom Server unterstützt."), ("Not available", "Nicht verfügbar"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 9c9097f6e..99752b3b6 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "La poŝo estas malplena"), ("Stop service", "Haltu servon"), ("Change ID", "Ŝanĝi identigilon"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Nur la signoj a-z, A-Z, 0-9, _ (substreko) povas esti uzataj. La unua litero povas esti inter a-z, A-Z. La longeco devas esti inter 6 kaj 16."), ("Website", "Retejo"), ("About", "Pri"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servilo de API"), ("invalid_http", "Devas komenci kun http:// aŭ https://"), ("Invalid IP", "IP nevalida"), - ("id_change_tip", "Nur la signoj a-z, A-Z, 0-9, _ (substreko) povas esti uzataj. La unua litero povas esti inter a-z, A-Z. La longeco devas esti inter 6 kaj 16."), ("Invalid format", "Formato nevalida"), ("server_not_support", "Ankoraŭ ne subtenata de la servilo"), ("Not available", "Nedisponebla"), diff --git a/src/lang/es.rs b/src/lang/es.rs index 63c1d26fc..ac367898f 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "El portapapeles está vacío"), ("Stop service", "Detener servicio"), ("Change ID", "Cambiar ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9 e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 y 16 caracteres."), ("Website", "Sitio web"), ("About", "Acerca de"), ("Slogan_tip", "Hecho con corazón en este mundo caótico!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servidor API"), ("invalid_http", "debe comenzar con http:// o https://"), ("Invalid IP", "IP incorrecta"), - ("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9 e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 y 16 caracteres."), ("Invalid format", "Formato incorrecto"), ("server_not_support", "Aún no es compatible con el servidor"), ("Not available", "No disponible"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index db565fe28..1d2fbe529 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "کلیپبورد خالی است"), ("Stop service", "توقف سرویس"), ("Change ID", "تعویض شناسه"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "شناسه باید طبق این شرایط باشد : حروف کوچک و بزرگ انگلیسی و اعداد از 0 تا 9، _ و همچنین حرف اول آن فقط حروف بزرگ یا کوچک انگلیسی و طول آن بین 6 الی 16 کاراکتر باشد"), ("Website", "وب سایت"), ("About", "درباره"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API سرور"), ("invalid_http", "شروع شود http:// یا https:// باید با"), ("Invalid IP", "نامعتبر است IP آدرس"), - ("id_change_tip", "شناسه باید طبق این شرایط باشد : حروف کوچک و بزرگ انگلیسی و اعداد از 0 تا 9، _ و همچنین حرف اول آن فقط حروف بزرگ یا کوچک انگلیسی و طول آن بین 6 الی 16 کاراکتر باشد"), ("Invalid format", "فرمت نادرست است"), ("server_not_support", "هنوز توسط سرور مورد نظر پشتیبانی نمی شود"), ("Not available", "در دسترسی نیست"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index fd46b4cf2..ef76a8fc1 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Presse-papier vide"), ("Stop service", "Arrêter le service"), ("Change ID", "Changer d'ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur doit être comprise entre 6 et 16."), ("Website", "Site Web"), ("About", "À propos de"), ("Slogan_tip", "Fait avec cœur dans ce monde chaotique!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Serveur API"), ("invalid_http", "Doit commencer par http:// ou https://"), ("Invalid IP", "IP invalide"), - ("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur doit être comprise entre 6 et 16."), ("Invalid format", "Format invalide"), ("server_not_support", "Pas encore supporté par le serveur"), ("Not available", "Indisponible"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 90c8e105a..9a813cd0a 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Το πρόχειρο είναι κενό"), ("Stop service", "Διακοπή υπηρεσίας"), ("Change ID", "Αλλαγή αναγνωριστικού ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9 και _ (υπογράμμιση). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."), ("Website", "Ιστότοπος"), ("About", "Πληροφορίες"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Διακομιστής API"), ("invalid_http", "Πρέπει να ξεκινά με http:// ή https://"), ("Invalid IP", "Μη έγκυρη διεύθυνση IP"), - ("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9 και _ (υπογράμμιση). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."), ("Invalid format", "Μη έγκυρη μορφή"), ("server_not_support", "Αυτή η δυνατότητα δεν υποστηρίζεται ακόμη από τον διακομιστή"), ("Not available", "Μη διαθέσιμο"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 78648a034..31a6d8d19 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "A vágólap üres"), ("Stop service", "Szolgáltatás leállítása"), ("Change ID", "Azonosító megváltoztatása"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Csak a-z, A-Z, 0-9 csoportokba tartozó karakterek, illetve a _ karakter van engedélyezve. Az első karakternek mindenképpen a-z, A-Z csoportokba kell esnie. Az azonosító hosszúsága 6-tól, 16 karakter."), ("Website", "Weboldal"), ("About", "Rólunk"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API szerver"), ("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."), ("Invalid IP", "A megadott IP cím helytelen."), - ("id_change_tip", "Csak a-z, A-Z, 0-9 csoportokba tartozó karakterek, illetve a _ karakter van engedélyezve. Az első karakternek mindenképpen a-z, A-Z csoportokba kell esnie. Az azonosító hosszúsága 6-tól, 16 karakter."), ("Invalid format", "Érvénytelen formátum"), ("server_not_support", "Nem támogatott a szerver által"), ("Not available", "Nem elérhető"), diff --git a/src/lang/id.rs b/src/lang/id.rs index d06cc649a..8176c9bc5 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Papan klip kosong"), ("Stop service", "Hentikan Layanan"), ("Change ID", "Ubah ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Hanya karakter a-z, A-Z, 0-9 dan _ (underscore) yang diperbolehkan. Huruf pertama harus a-z, A-Z. Panjang antara 6 dan 16."), ("Website", "Website"), ("About", "Tentang"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Server"), ("invalid_http", "harus dimulai dengan http:// atau https://"), ("Invalid IP", "IP tidak valid"), - ("id_change_tip", "Hanya karakter a-z, A-Z, 0-9 dan _ (underscore) yang diperbolehkan. Huruf pertama harus a-z, A-Z. Panjang antara 6 dan 16."), ("Invalid format", "Format tidak valid"), ("server_not_support", "Belum didukung oleh server"), ("Not available", "Tidak tersedia"), diff --git a/src/lang/it.rs b/src/lang/it.rs index ab0c8064c..2431da441 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Gli appunti sono vuoti"), ("Stop service", "Arresta servizio"), ("Change ID", "Cambia ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (underscore). Il primo carattere deve essere a-z o A-Z. La lunghezza deve essere fra 6 e 16 caratteri."), ("Website", "Sito web"), ("About", "Informazioni"), ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Server API"), ("invalid_http", "deve iniziare con http:// o https://"), ("Invalid IP", "Indirizzo IP non valido"), - ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (underscore). Il primo carattere deve essere a-z o A-Z. La lunghezza deve essere fra 6 e 16 caratteri."), ("Invalid format", "Formato non valido"), ("server_not_support", "Non ancora supportato dal server"), ("Not available", "Non disponibile"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 6e72d4b04..a51795236 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "クリップボードは空です"), ("Stop service", "サービスを停止"), ("Change ID", "IDを変更"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "使用できるのは大文字・小文字のアルファベット、数字、アンダースコア(_)のみです。初めの文字はアルファベットにする必要があります。6文字から16文字までです。"), ("Website", "公式サイト"), ("About", "情報"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "APIサーバー"), ("invalid_http", "http:// もしくは https:// から入力してください"), ("Invalid IP", "無効なIP"), - ("id_change_tip", "使用できるのは大文字・小文字のアルファベット、数字、アンダースコア(_)のみです。初めの文字はアルファベットにする必要があります。6文字から16文字までです。"), ("Invalid format", "無効な形式"), ("server_not_support", "サーバー側でまだサポートされていません"), ("Not available", "利用不可"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index b7b59ed9c..b6e992fad 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "클립보드가 비어있습니다"), ("Stop service", "서비스 중단"), ("Change ID", "ID 변경"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "a-z, A-Z, 0-9, _(밑줄 문자)만 입력 가능합니다. 첫 문자는 a-z 혹은 A-Z로 시작해야 합니다. 길이는 6 ~ 16글자가 요구됩니다."), ("Website", "웹사이트"), ("About", "정보"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API 서버"), ("invalid_http", "다음과 같이 시작해야 합니다. http:// 또는 https://"), ("Invalid IP", "유효하지 않은 IP"), - ("id_change_tip", "a-z, A-Z, 0-9, _(밑줄 문자)만 입력 가능합니다. 첫 문자는 a-z 혹은 A-Z로 시작해야 합니다. 길이는 6 ~ 16글자가 요구됩니다."), ("Invalid format", "유효하지 않은 형식"), ("server_not_support", "해당 서버가 아직 지원하지 않습니다"), ("Not available", "불가능"), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 9fdc29260..aafec8b01 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Көшіру-тақта бос"), ("Stop service", "Сербесті тоқтату"), ("Change ID", "ID ауыстыру"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Тек a-z, A-Z, 0-9 және _ (астынғы-сызық) таңбалары рұқсат етілген. Бірінші таңба a-z, A-Z болуы қажет. Ұзындығы 6 мен 16 арасы."), ("Website", "Web-сайт"), ("About", "Туралы"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Сербері"), ("invalid_http", "http:// немесе https://'пен басталуы қажет"), ("Invalid IP", "Бұрыс IP-Мекенжай"), - ("id_change_tip", "Тек a-z, A-Z, 0-9 және _ (астынғы-сызық) таңбалары рұқсат етілген. Бірінші таңба a-z, A-Z болуы қажет. Ұзындығы 6 мен 16 арасы."), ("Invalid format", "Бұрыс формат"), ("server_not_support", "Сербер әзірше қолдамайды"), ("Not available", "Қолжетімсіз"), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 2502cb34c..9a239238d 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Klembord is leeg"), ("Stop service", "Stop service"), ("Change ID", "Wijzig ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), ("Website", "Website"), ("About", "Over"), ("Slogan_tip", "Gedaan met het hart in deze chaotische wereld!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Server"), ("invalid_http", "Moet beginnen met http:// of https://"), ("Invalid IP", "Ongeldig IP"), - ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), ("Invalid format", "Ongeldig formaat"), ("server_not_support", "Nog niet ondersteund door de server"), ("Not available", "Niet beschikbaar"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 24563d21f..be61e94ec 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Schowek jest pusty"), ("Stop service", "Zatrzymaj usługę"), ("Change ID", "Zmień ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Nowy ID może być złożony z małych i dużych liter a-zA-z, cyfry 0-9 oraz _ (podkreślenie). Pierwszym znakiem powinna być litera a-zA-Z, a całe ID powinno składać się z 6 do 16 znaków."), ("Website", "Strona internetowa"), ("About", "O aplikacji"), ("Slogan_tip", "Tworzone z miłością w tym pełnym chaosu świecie!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Serwer API"), ("invalid_http", "Nieprawidłowe żądanie http"), ("Invalid IP", "Nieprawidłowe IP"), - ("id_change_tip", "Nowy ID może być złożony z małych i dużych liter a-zA-z, cyfry 0-9 oraz _ (podkreślenie). Pierwszym znakiem powinna być litera a-zA-Z, a całe ID powinno składać się z 6 do 16 znaków."), ("Invalid format", "Nieprawidłowy format"), ("server_not_support", "Serwer nie obsługuje tej funkcji"), ("Not available", "Niedostępne"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 078bf3761..b4befcdcb 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "A área de transferência está vazia"), ("Stop service", "Parar serviço"), ("Change ID", "Alterar ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Website", "Website"), ("About", "Sobre"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servidor da API"), ("invalid_http", "deve iniciar com http:// ou https://"), ("Invalid IP", "IP inválido"), - ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Invalid format", "Formato inválido"), ("server_not_support", "Ainda não suportado pelo servidor"), ("Not available", "Indisponível"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index e08700d44..3fe0ca868 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "A área de transferência está vazia"), ("Stop service", "Parar serviço"), ("Change ID", "Alterar ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Website", "Website"), ("About", "Sobre"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servidor da API"), ("invalid_http", "deve iniciar com http:// ou https://"), ("Invalid IP", "IP inválido"), - ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Invalid format", "Formato inválido"), ("server_not_support", "Ainda não suportado pelo servidor"), ("Not available", "Indisponível"), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 5be2a914a..b06d1fa0c 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Clipboard gol"), ("Stop service", "Oprește serviciu"), ("Change ID", "Schimbă ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Pot fi utilizate doar caractere a-z, A-Z, 0-9, _ (bară jos). Primul caracter trebuie să fie a-z, A-Z. Lungimea trebuie să fie între 6 și 16 caractere."), ("Website", "Site web"), ("About", "Despre"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Server API"), ("invalid_http", "Trebuie să înceapă cu http:// sau https://"), ("Invalid IP", "IP nevalid"), - ("id_change_tip", "Pot fi utilizate doar caractere a-z, A-Z, 0-9, _ (bară jos). Primul caracter trebuie să fie a-z, A-Z. Lungimea trebuie să fie între 6 și 16 caractere."), ("Invalid format", "Format nevalid"), ("server_not_support", "Încă nu este compatibil cu serverul"), ("Not available", "Indisponibil"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index c389d6821..9746e8a41 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Буфер обмена пуст"), ("Stop service", "Остановить службу"), ("Change ID", "Изменить ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Допускаются только символы a-z, A-Z, 0-9 и _ (подчёркивание). Первой должна быть буква a-z, A-Z. Длина от 6 до 16."), ("Website", "Сайт"), ("About", "О программе"), ("Slogan_tip", "Сделано с душой в этом безумном мире!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API-сервер"), ("invalid_http", "Должен начинаться с http:// или https://"), ("Invalid IP", "Неправильный IP-адрес"), - ("id_change_tip", "Допускаются только символы a-z, A-Z, 0-9 и _ (подчёркивание). Первой должна быть буква a-z, A-Z. Длина от 6 до 16."), ("Invalid format", "Неправильный формат"), ("server_not_support", "Пока не поддерживается сервером"), ("Not available", "Недоступно"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index bf4b85b1b..27bf78dd7 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Schránka je prázdna"), ("Stop service", "Zastaviť službu"), ("Change ID", "Zmeniť ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Povolené sú len znaky a-z, A-Z, 0-9 a _ (podčiarkovník). Prvý znak musí byť a-z, A-Z. Dĺžka musí byť medzi 6 a 16 znakmi."), ("Website", "Webová stránka"), ("About", "O RustDesk"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API server"), ("invalid_http", "Musí začínať http:// alebo https://"), ("Invalid IP", "Neplatná IP adresa"), - ("id_change_tip", "Povolené sú len znaky a-z, A-Z, 0-9 a _ (podčiarkovník). Prvý znak musí byť a-z, A-Z. Dĺžka musí byť medzi 6 a 16 znakmi."), ("Invalid format", "Neplatný formát"), ("server_not_support", "Zatiaľ serverom nepodporované"), ("Not available", "Nie je k dispozícii"), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index f464cb8fc..4ccc9e35f 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Odložišče je prazno"), ("Stop service", "Ustavi storitev"), ("Change ID", "Spremeni ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Dovoljeni znaki so a-z, A-Z (brez šumnikov), 0-9 in _. Prvi znak mora biti črka, dolžina od 6 do 16 znakov."), ("Website", "Spletna stran"), ("About", "O programu"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API strežnik"), ("invalid_http", "mora se začeti s http:// ali https://"), ("Invalid IP", "Neveljaven IP"), - ("id_change_tip", "Dovoljeni znaki so a-z, A-Z (brez šumnikov), 0-9 in _. Prvi znak mora biti črka, dolžina od 6 do 16 znakov."), ("Invalid format", "Neveljavna oblika"), ("server_not_support", "Strežnik še ne podpira"), ("Not available", "Ni na voljo"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index a6b83d9f3..347d12794 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Clipboard është bosh"), ("Stop service", "Ndaloni shërbimin"), ("Change ID", "Ndryshoni ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Lejohen Vetëm karkteret a-z,A-Z,0-9 dhe _(nënvizimet).Shkronja e parë duhet të jetë a-z, A-Z. Gjatesia midis 6 dhe 16."), ("Website", "Faqe ëebi"), ("About", "Rreth"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Serveri API"), ("invalid_http", "Duhet të fillojë me http:// ose https://"), ("Invalid IP", "IP e pavlefshme"), - ("id_change_tip", "Lejohen Vetëm karkteret a-z,A-Z,0-9 dhe _(nënvizimet).Shkronja e parë duhet të jetë a-z, A-Z. Gjatesia midis 6 dhe 16."), ("Invalid format", "Format i pavlefshëm"), ("server_not_support", "Nuk suportohet akoma nga severi"), ("Not available", "I padisponueshëm"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 09c34b4fc..19232b1e9 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Clipboard je prazan"), ("Stop service", "Stopiraj servis"), ("Change ID", "Promeni ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Dozvoljeni su samo a-z, A-Z, 0-9 i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Dužina je od 6 do 16."), ("Website", "Web sajt"), ("About", "O programu"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API server"), ("invalid_http", "mora početi sa http:// ili https://"), ("Invalid IP", "Nevažeća IP"), - ("id_change_tip", "Dozvoljeni su samo a-z, A-Z, 0-9 i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Dužina je od 6 do 16."), ("Invalid format", "Pogrešan format"), ("server_not_support", "Server još uvek ne podržava"), ("Not available", "Nije dostupno"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 2154b2729..da7f4df43 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Urklippet är tomt"), ("Stop service", "Avsluta tjänsten"), ("Change ID", "Byt ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Bara a-z, A-Z, 0-9 och _ (understräck) tecken är tillåtna. Den första bokstaven måste vara a-z, A-Z. Längd mellan 6 och 16."), ("Website", "Hemsida"), ("About", "Om"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Server"), ("invalid_http", "måste börja med http:// eller https://"), ("Invalid IP", "Ogiltig IP"), - ("id_change_tip", "Bara a-z, A-Z, 0-9 och _ (understräck) tecken är tillåtna. Den första bokstaven måste vara a-z, A-Z. Längd mellan 6 och 16."), ("Invalid format", "Ogiltigt format"), ("server_not_support", "Stöds ännu inte av servern"), ("Not available", "Ej tillgänglig"), diff --git a/src/lang/template.rs b/src/lang/template.rs index f46a301f6..e988b648c 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", ""), ("Stop service", ""), ("Change ID", ""), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", ""), ("Website", ""), ("About", ""), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", ""), ("invalid_http", ""), ("Invalid IP", ""), - ("id_change_tip", ""), ("Invalid format", ""), ("server_not_support", ""), ("Not available", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index 93e984be3..570806412 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "คลิปบอร์ดว่างเปล่า"), ("Stop service", "หยุดการใช้งานเซอร์วิส"), ("Change ID", "เปลี่ยน ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "อนุญาตเฉพาะตัวอักษร a-z A-Z 0-9 และ _ (ขีดล่าง) เท่านั้น โดยตัวอักษรขึ้นต้นจะต้องเป็น a-z หรือไม่ก็ A-Z และมีความยาวระหว่าง 6 ถึง 16 ตัวอักษร"), ("Website", "เว็บไซต์"), ("About", "เกี่ยวกับ"), ("Slogan_tip", "ทำด้วยใจ ในโลกใบนี้ที่ยุ่งเหยิง!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "เซิร์ฟเวอร์ API"), ("invalid_http", "ต้องขึ้นต้นด้วย http:// หรือ https:// เท่านั้น"), ("Invalid IP", "IP ไม่ถูกต้อง"), - ("id_change_tip", "อนุญาตเฉพาะตัวอักษร a-z A-Z 0-9 และ _ (ขีดล่าง) เท่านั้น โดยตัวอักษรขึ้นต้นจะต้องเป็น a-z หรือไม่ก็ A-Z และมีความยาวระหว่าง 6 ถึง 16 ตัวอักษร"), ("Invalid format", "รูปแบบไม่ถูกต้อง"), ("server_not_support", "ยังไม่รองรับโดยเซิร์ฟเวอร์"), ("Not available", "ไม่พร้อมใช้งาน"), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 214ee83df..393357ece 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Kopyalanan geçici veri boş"), ("Stop service", "Servisi Durdur"), ("Change ID", "ID Değiştir"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Yalnızca a-z, A-Z, 0-9 ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), ("Website", "Website"), ("About", "Hakkında"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Sunucu"), ("invalid_http", "http:// veya https:// ile başlamalıdır"), ("Invalid IP", "Geçersiz IP adresi"), - ("id_change_tip", "Yalnızca a-z, A-Z, 0-9 ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), ("Invalid format", "Hatalı Format"), ("server_not_support", "Henüz sunucu tarafından desteklenmiyor"), ("Not available", "Erişilebilir değil"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index db26e5387..17cafb8f0 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "剪貼簿是空的"), ("Stop service", "停止服務"), ("Change ID", "更改 ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "僅能使用以下字元:a-z、A-Z、0-9、_ (底線)。首字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), ("Website", "網站"), ("About", "關於"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API 伺服器"), ("invalid_http", "開頭必須為 http:// 或 https://"), ("Invalid IP", "IP 無效"), - ("id_change_tip", "僅能使用以下字元:a-z、A-Z、0-9、_ (底線)。首字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), ("Invalid format", "格式無效"), ("server_not_support", "服務器暫不支持"), ("Not available", "無法使用"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index c3894726a..7eeca7deb 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Буфер обміну порожній"), ("Stop service", "Зупинити службу"), ("Change ID", "Змінити ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Допускаються тільки символи a-z, A-Z, 0-9 і _ (підкреслення). Перша буква повинна бути a-z, A-Z. Довжина від 6 до 16"), ("Website", "Веб-сайт"), ("About", "Про RustDesk"), ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API-сервер"), ("invalid_http", "Повинен починатися з http:// або https://"), ("Invalid IP", "Невірна IP-адреса"), - ("id_change_tip", "Допускаються тільки символи a-z, A-Z, 0-9 і _ (підкреслення). Перша буква повинна бути a-z, A-Z. Довжина від 6 до 16"), ("Invalid format", "Невірний формат"), ("server_not_support", "Поки не підтримується сервером"), ("Not available", "Недоступно"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 45c2cc519..3affb52d2 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -37,6 +37,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Khay nhớ tạm trống"), ("Stop service", "Dừng dịch vụ"), ("Change ID", "Thay đổi ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Các kí tự đuợc phép là: từ a-z, A-Z, 0-9 và _ (dấu gạch dưới). Kí tự đầu tiên phải bắt đầu từ a-z, A-Z. Độ dài kí tự từ 6 đến 16"), ("Website", "Trang web"), ("About", "About"), ("Slogan_tip", ""), @@ -54,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Máy chủ API"), ("invalid_http", "phải bắt đầu bằng http:// hoặc https://"), ("Invalid IP", "IP không hợp lệ"), - ("id_change_tip", "Các kí tự đuợc phép là: từ a-z, A-Z, 0-9 và _ (dấu gạch dưới). Kí tự đầu tiên phải bắt đầu từ a-z, A-Z. Độ dài kí tự từ 6 đến 16"), ("Invalid format", "Định dạng không hợp lệnh"), ("server_not_support", "Chưa đuợc hỗ trợ bới server"), ("Not available", "Chưa có mặt"), From b4d4b4249e2c43db6abe7865a02b1f1545f50c5a Mon Sep 17 00:00:00 2001 From: grummbeer Date: Wed, 15 Feb 2023 13:43:38 +0100 Subject: [PATCH 121/202] unifiy left labeled text input --- flutter/lib/common/widgets/peer_card.dart | 41 ++++++---------- .../desktop/pages/desktop_setting_page.dart | 48 +++++++------------ 2 files changed, 32 insertions(+), 57 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 3c9a438a0..f1b94ecdf 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -996,14 +996,11 @@ void _rdpDialog(String id) async { Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 140), child: Text( "${translate('Port')}:", - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: TextField( inputFormatters: [ @@ -1017,21 +1014,15 @@ void _rdpDialog(String id) async { ), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: 8), Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 140), child: Text( "${translate('Username')}:", - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: TextField( decoration: @@ -1040,19 +1031,15 @@ void _rdpDialog(String id) async { ), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: 8), Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text("${translate('Password')}:") - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Password')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: Obx(() => TextField( obscureText: secure.value, @@ -1067,7 +1054,7 @@ void _rdpDialog(String id) async { )), ), ], - ), + ).marginOnly(bottom: 8), ], ), ), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 34398dd0d..187ffc9fc 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1856,12 +1856,11 @@ void changeSocks5Proxy() async { Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Hostname")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate("Hostname")}:', + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: TextField( decoration: InputDecoration( @@ -1872,19 +1871,15 @@ void changeSocks5Proxy() async { ), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: 8), Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Username")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate("Username")}:', + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: TextField( decoration: const InputDecoration( @@ -1894,19 +1889,15 @@ void changeSocks5Proxy() async { ), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: 8), Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Password")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate("Password")}:', + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: Obx(() => TextField( obscureText: obscure.value, @@ -1921,10 +1912,7 @@ void changeSocks5Proxy() async { )), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: 8), Offstage( offstage: !isInProgress, child: const LinearProgressIndicator()) ], From 95ff8e4bbd3fc015a7f5b90dfb824c49e5cce040 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Sun, 19 Feb 2023 18:00:58 +0100 Subject: [PATCH 122/202] unifiy left labeled text input server --- .../desktop/pages/desktop_setting_page.dart | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 187ffc9fc..971c713ce 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1074,7 +1074,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { Row( mainAxisAlignment: MainAxisAlignment.end, children: [_Button('Apply', submit, enabled: enabled)], - ).marginOnly(top: 15), + ).marginOnly(top: 10), ], ) ]); @@ -1697,33 +1697,30 @@ _LabeledTextField( bool secure) { return Row( children: [ - Spacer(flex: 1), + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate(label)}:', + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 16, color: _disabledTextColor(context, enabled)), + ).marginOnly(right: 10)), Expanded( - flex: 4, - child: Text( - '${translate(label)}:', - textAlign: TextAlign.right, - style: TextStyle(color: _disabledTextColor(context, enabled)), - ), - ), - Spacer(flex: 1), - Expanded( - flex: 10, child: TextField( controller: controller, enabled: enabled, obscureText: secure, decoration: InputDecoration( isDense: true, - contentPadding: EdgeInsets.symmetric(vertical: 15), + border: OutlineInputBorder(), + contentPadding: EdgeInsets.fromLTRB(14, 15, 14, 15), errorText: errorText.isNotEmpty ? errorText : null), style: TextStyle( color: _disabledTextColor(context, enabled), )), ), - Spacer(flex: 1), ], - ); + ).marginOnly(bottom: 8); } // ignore: must_be_immutable From 25dba291ef387a7230669ebcaf0aa2fb8e30308d Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Sun, 19 Feb 2023 18:23:58 +0000 Subject: [PATCH 123/202] steps to automate --- .devcontainer/Dockerfile | 10 +++++++++- flutter/build_android.sh | 8 ++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 6b86e88d2..6d00302f7 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -39,4 +39,12 @@ RUN sudo apt install -y gcc-multilib WORKDIR $WORKDIR ENV ANDROID_NDK_HOME=/opt/android/ndk/22.1.7171670 -# Somehow try to automate flutter pub get \ No newline at end of file + +# Somehow try to automate flutter pub get +# https://rustdesk.com/docs/en/dev/build/android/ +# Put below steps in entrypoint.sh +# cd flutter +# wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz +# tar xzf so.tar.gz + +# own /opt/android diff --git a/flutter/build_android.sh b/flutter/build_android.sh index 01ff23488..0a2854299 100755 --- a/flutter/build_android.sh +++ b/flutter/build_android.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -$ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* -flutter build apk --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info -flutter build apk ---split-per-abi --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info -flutter build appbundle --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info +$ANDROID_NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* +#flutter build apk --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info +flutter build apk --split-per-abi --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info +#flutter build appbundle --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info # build in linux # $ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* From 9cdc66dcdf2e330f6ee6abe9b614f64152f4873e Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Mon, 20 Feb 2023 02:17:14 +0100 Subject: [PATCH 124/202] Update es.rs --- src/lang/es.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 63c1d26fc..3a467cb16 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -415,7 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Si tienes una gráfica Nvidia y la ventana remota se cierra inmediatamente, instalar el driver nouveau y elegir renderizado por software podría ayudar. Se requiere reiniciar la aplicación."), ("Always use software rendering", "Usar siempre renderizado por software"), ("config_input", "Para controlar el escritorio remoto con el teclado necesitas dar a RustDesk permisos de \"Monitorización de entrada\"."), - ("config_microphone", ""), + ("config_microphone", "Para poder hablar de forma remota necesitas darle a RustDesk permisos de \"Grabar Audio\"."), ("request_elevation_tip", "También puedes solicitar elevación si hay alguien en el lado remoto."), ("Wait", "Esperar"), ("Elevation Error", "Error de elevación"), @@ -436,7 +436,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Fuerte"), ("Switch Sides", "Intercambiar lados"), ("Please confirm if you want to share your desktop?", "Por favor, confirma si quieres compartir tu escritorio"), - ("Closed as expected", ""), + ("Closed as expected", "Cerrado como se esperaba"), ("Display", "Pantalla"), ("Default View Style", "Estilo de vista predeterminado"), ("Default Scroll Style", "Estilo de desplazamiento predeterminado"), From d18fc32f63401dcf57acaa508592b3fd0aad2575 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 20 Feb 2023 10:45:34 +0800 Subject: [PATCH 125/202] fix #3263 --- src/client.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8683dad1f..6e4033d74 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2213,8 +2213,7 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b && !text.to_lowercase().contains("mismatch") && !text.to_lowercase().contains("manually") && !text.to_lowercase().contains("not allowed") - && !text.to_lowercase().contains("as expected") - && !text.to_lowercase().contains("reset by the peer"))) + && !text.to_lowercase().contains("as expected"))) } #[inline] From 4cef2c2d0cd6d89846feb07e022d74e25761604f Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Mon, 20 Feb 2023 08:48:39 +0100 Subject: [PATCH 126/202] Update it.rs --- src/lang/it.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 2431da441..2d66706d2 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -37,10 +37,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Gli appunti sono vuoti"), ("Stop service", "Arresta servizio"), ("Change ID", "Cambia ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Il tuo nuovo ID"), + ("length %min% to %max%", "da lunghezza %min% a %max%"), + ("starts with a letter", "inizia con una lettera"), + ("allowed characters", "caratteri consentiti"), ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (underscore). Il primo carattere deve essere a-z o A-Z. La lunghezza deve essere fra 6 e 16 caratteri."), ("Website", "Sito web"), ("About", "Informazioni"), @@ -213,7 +213,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Chiuso manualmente dal peer"), ("Enable remote configuration modification", "Abilita la modifica remota della configurazione"), ("Run without install", "Esegui senza installare"), - ("Connect via relay", ""), + ("Connect via relay", "Collegati tramite relay"), ("Always connect via relay", "Collegati sempre tramite relay"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("Login", "Accedi"), @@ -419,7 +419,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Se si dispone di una scheda grafica Nvidia e la finestra remota si chiude immediatamente dopo la connessione, l'installazione del driver nouveau e la scelta di utilizzare il rendering software possono aiutare. È necessario un riavvio del software."), ("Always use software rendering", "Usa sempre il render Software"), ("config_input", "Per controllare il desktop remoto con la tastiera, è necessario concedere le autorizzazioni a RustDesk \"Monitoraggio dell'input\"."), - ("config_microphone", ""), + ("config_microphone", "Per poter chiamare, è necessario concedere l'autorizzazione a RustDesk \"Registra audio\"."), ("request_elevation_tip", "È possibile richiedere l'elevazione se c'è qualcuno sul lato remoto."), ("Wait", "Attendi"), ("Elevation Error", "Errore durante l'elevazione dei diritti"), @@ -448,12 +448,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Default Codec", "Codec Predefinito"), ("Bitrate", "Bitrate"), ("FPS", "FPS"), - ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), ("Voice call", "Chiamata vocale"), ("Text chat", "Chat testuale"), ("Stop voice call", "Interrompi la chiamata vocale"), - ("relay_hint_tip", ""), + ("relay_hint_tip", "Se non è possibile connettersi direttamente, si può provare a farlo tramite relay.\nInoltre, se si desidera utilizzare il relay al primo tentativo, è possibile aggiungere il suffisso \"/r\" all'ID o selezionare l'opzione \"Collegati sempre tramite relay\" nella scheda peer."), ("Reconnect", "Riconnetti"), ].iter().cloned().collect(); } From 13b1b78f72c49d4af93d8e1bf370d011c047a6c3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 20 Feb 2023 15:54:53 +0800 Subject: [PATCH 127/202] remove closed as expected on switchsides, which makes second prompt Signed-off-by: 21pages --- src/server/connection.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 9cdbf974c..d2eb21ee5 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1593,7 +1593,6 @@ impl Connection { uuid.to_string().as_ref(), ]) .ok(); - self.send_close_reason_no_retry("Closed as expected").await; self.on_close("switch sides", false).await; return false; } From 172b1d5e2ddc1bb8b4f632827ec1b733144e735e Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Mon, 20 Feb 2023 09:11:38 +0100 Subject: [PATCH 128/202] Removed by mistake --- src/lang/it.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lang/it.rs b/src/lang/it.rs index 2d66706d2..68ec10807 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -448,6 +448,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Default Codec", "Codec Predefinito"), ("Bitrate", "Bitrate"), ("FPS", "FPS"), + ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), ("Voice call", "Chiamata vocale"), ("Text chat", "Chat testuale"), From 1af71cc5f36c93ca91c6906ae6b0b0cd6427865d Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 20 Feb 2023 16:12:11 +0800 Subject: [PATCH 129/202] remove all other "as expected" Signed-off-by: 21pages --- src/client.rs | 3 +-- src/lang/ca.rs | 1 - src/lang/cn.rs | 1 - src/lang/cs.rs | 1 - src/lang/da.rs | 1 - src/lang/de.rs | 1 - src/lang/eo.rs | 1 - src/lang/es.rs | 1 - src/lang/fa.rs | 1 - src/lang/fr.rs | 1 - src/lang/gr.rs | 1 - src/lang/hu.rs | 1 - src/lang/id.rs | 1 - src/lang/it.rs | 1 - src/lang/ja.rs | 1 - src/lang/ko.rs | 1 - src/lang/kz.rs | 1 - src/lang/nl.rs | 1 - src/lang/pl.rs | 1 - src/lang/pt_PT.rs | 1 - src/lang/ptbr.rs | 1 - src/lang/ro.rs | 1 - src/lang/ru.rs | 1 - src/lang/sk.rs | 1 - src/lang/sl.rs | 1 - src/lang/sq.rs | 1 - src/lang/sr.rs | 1 - src/lang/sv.rs | 1 - src/lang/template.rs | 1 - src/lang/th.rs | 1 - src/lang/tr.rs | 1 - src/lang/tw.rs | 1 - src/lang/ua.rs | 1 - src/lang/vn.rs | 1 - 34 files changed, 1 insertion(+), 35 deletions(-) diff --git a/src/client.rs b/src/client.rs index 6e4033d74..f36bdae78 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2212,8 +2212,7 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b && !text.to_lowercase().contains("resolve") && !text.to_lowercase().contains("mismatch") && !text.to_lowercase().contains("manually") - && !text.to_lowercase().contains("not allowed") - && !text.to_lowercase().contains("as expected"))) + && !text.to_lowercase().contains("not allowed"))) } #[inline] diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 0d1eeff13..45c552848 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 63b59e8f1..9d0d176da 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "强"), ("Switch Sides", "反转访问方向"), ("Please confirm if you want to share your desktop?", "请确认要让对方访问你的桌面?"), - ("Closed as expected", "正常关闭"), ("Display", "显示"), ("Default View Style", "默认显示方式"), ("Default Scroll Style", "默认滚动方式"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index f4d63cba9..e2761e45e 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index b3bf02dd2..2020a2b6f 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index ddc347605..7cf563fc3 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Stark"), ("Switch Sides", "Seiten wechseln"), ("Please confirm if you want to share your desktop?", "Bitte bestätigen Sie, ob Sie Ihren Desktop freigeben möchten."), - ("Closed as expected", "Wie erwartet geschlossen"), ("Display", "Anzeige"), ("Default View Style", "Standard-Ansichtsstil"), ("Default Scroll Style", "Standard-Scroll-Stil"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 99752b3b6..c22532440 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index 599da6fbf..3ce2860f0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Fuerte"), ("Switch Sides", "Intercambiar lados"), ("Please confirm if you want to share your desktop?", "Por favor, confirma si quieres compartir tu escritorio"), - ("Closed as expected", "Cerrado como se esperaba"), ("Display", "Pantalla"), ("Default View Style", "Estilo de vista predeterminado"), ("Default Scroll Style", "Estilo de desplazamiento predeterminado"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 1d2fbe529..00f6b70ac 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "قوی"), ("Switch Sides", "طرفین را عوض کنید"), ("Please confirm if you want to share your desktop?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), - ("Closed as expected", "طبق انتظار بسته شد"), ("Display", "نمایش دادن"), ("Default View Style", "سبک نمایش پیش فرض"), ("Default Scroll Style", "سبک پیش‌فرض اسکرول"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index ef76a8fc1..1f6e9f55b 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Fort"), ("Switch Sides", "Inverser la prise de contrôle"), ("Please confirm if you want to share your desktop?", "Veuillez confirmer le partager de votre bureau ?"), - ("Closed as expected", "Fermé normalement"), ("Display", "Affichage"), ("Default View Style", "Style de vue par défaut"), ("Default Scroll Style", "Style de défilement par défaut"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 9a813cd0a..b7ebf4577 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Δυνατό"), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 31a6d8d19..21ab28214 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/id.rs b/src/lang/id.rs index 8176c9bc5..f48de17f6 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/it.rs b/src/lang/it.rs index 68ec10807..4c63106da 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Forte"), ("Switch Sides", "Cambia lato"), ("Please confirm if you want to share your desktop?", "Vuoi condividere il tuo desktop?"), - ("Closed as expected", "Chiuso come previsto"), ("Display", "Visualizzazione"), ("Default View Style", "Stile Visualizzazione Predefinito"), ("Default Scroll Style", "Stile Scorrimento Predefinito"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a51795236..b291a6e7a 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index b6e992fad..d63e83187 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index aafec8b01..b8b9eb1df 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 9a239238d..1a806c803 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Sterk"), ("Switch Sides", "Wissel van kant"), ("Please confirm if you want to share your desktop?", "bevestig als je je bureaublad wilt delen?"), - ("Closed as expected", "Gesloten zoals verwacht"), ("Display", "Weergave"), ("Default View Style", "Standaard Weergave Stijl"), ("Default Scroll Style", "Standaard Scroll Stijl"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index be61e94ec..2b29c7cb2 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Mocne"), ("Switch Sides", "Zamień Strony"), ("Please confirm if you want to share your desktop?", "Czy na pewno chcesz udostępnić swój ekran?"), - ("Closed as expected", "Zamknięto pomyślnie"), ("Display", "Wyświetlanie"), ("Default View Style", "Domyślny styl wyświetlania"), ("Default Scroll Style", "Domyślny styl przewijania"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index b4befcdcb..e91cd3909 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 3fe0ca868..b0fe9175d 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index b06d1fa0c..d0232ba37 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 9746e8a41..6df73f1eb 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Стойкий"), ("Switch Sides", "Переключить стороны"), ("Please confirm if you want to share your desktop?", "Подтверждаете, что хотите поделиться своим рабочим столом?"), - ("Closed as expected", "Закрыто по ожиданию"), ("Display", "Отображение"), ("Default View Style", "Стиль отображения по умолчанию"), ("Default Scroll Style", "Стиль прокрутки по умолчанию"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 27bf78dd7..458002f4c 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 4ccc9e35f..2abd1870f 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 347d12794..6b739e8ab 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 19232b1e9..90a435fd7 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index da7f4df43..a98ea6346 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/template.rs b/src/lang/template.rs index e988b648c..61c2b5d28 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index 570806412..236ee5e8d 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 393357ece..f2a34e212 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 17cafb8f0..84e74716f 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "強"), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", "正常關閉"), ("Display", "顯示"), ("Default View Style", "默認顯示方式"), ("Default Scroll Style", "默認滾動方式"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 7eeca7deb..0c4caf4db 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 3affb52d2..19e1184d9 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -440,7 +440,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", ""), ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), From c76b971addb02f60e33032ce05d4635994c1de2e Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 20 Feb 2023 13:42:23 +0300 Subject: [PATCH 130/202] Update ru.rs --- src/lang/ru.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 9746e8a41..34a433461 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -37,10 +37,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Буфер обмена пуст"), ("Stop service", "Остановить службу"), ("Change ID", "Изменить ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Новый ID"), + ("length %min% to %max%", "длина %min%...%max%"), + ("starts with a letter", "начинается с буквы"), + ("allowed characters", "допустимые символы"), ("id_change_tip", "Допускаются только символы a-z, A-Z, 0-9 и _ (подчёркивание). Первой должна быть буква a-z, A-Z. Длина от 6 до 16."), ("Website", "Сайт"), ("About", "О программе"), From 355601396b03f781784d6ce64a5f900057bd4b90 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Mon, 20 Feb 2023 13:54:13 +0100 Subject: [PATCH 131/202] Fix wrong language alt --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 866063726..df0ca8328 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- RustDesk - Dit fjernskrivebord
+ RustDesk - Your remote desktop
ServersBuildDocker • From d08fa1fb11bbe3e180ceb1a15e38ee254d72a201 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Mon, 20 Feb 2023 15:30:36 +0000 Subject: [PATCH 132/202] setup --- .devcontainer/Dockerfile | 2 +- .devcontainer/build.sh | 73 +++++++++++++++++++++++++++++++++ .devcontainer/devcontainer.json | 6 ++- .devcontainer/setup.sh | 19 +++++++++ flutter/build_android.sh | 8 ++-- 5 files changed, 102 insertions(+), 6 deletions(-) create mode 100755 .devcontainer/build.sh create mode 100644 .devcontainer/setup.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 6d00302f7..32a440b28 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -28,7 +28,7 @@ RUN $HOME/.cargo/bin/cargo install cargo-ndk # Install Flutter RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.3-stable.tar.xz -RUN tar xf flutter_linux_3.7.3-stable.tar.xz +RUN tar xf flutter_linux_3.7.3-stable.tar.xz && rm flutter_linux_3.7.3-stable.tar.xz ENV PATH="$PATH:$HOME/flutter/bin" RUN dart pub global activate ffigen 5.0.1 diff --git a/.devcontainer/build.sh b/.devcontainer/build.sh new file mode 100755 index 000000000..a41d4dc38 --- /dev/null +++ b/.devcontainer/build.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +set -e + +MODE=${1:---debug} +TYPE=${2:-linux} +MODE=${MODE/*-/} + + +build(){ + pwd + $WORKDIR/entrypoint $1 +} + +build_arm64(){ + CWD=$(pwd) + cd $WORKDIR + $WORKDIR/flutter/ndk_arm64.sh + cp $WORKDIR/target/aarch64-linux-android/release/liblibrustdesk.so $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + cd $CWD +} + +build_apk(){ + cd $WORKDIR/flutter + MODE=$1 $WORKDIR/flutter/build_android.sh + cd $WORKDIR +} + +key_gen(){ + if [ ! -f $WORKDIR/flutter/android/key.properties ] + then + if [ ! -f $HOME/upload-keystore.jks ] + then + echo "Remember the password you enter in keytool!" + keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload + else + read -r -p "enter the password used to generate $HOME/upload-keystore.jks" password + echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties + fi + fi +} + +android_build(){ + if [ ! -d $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a ] + then + $WORKDIR/.devcontainer/setup.sh android + fi + build_arm64 + case $1 in + debug) + build_apk debug + ;; + release) + key_gen + build_apk release + ;; + esac +} + +case "$MODE:$TYPE" in + "debug:linux") + build + ;; + "release:linux") + build --release + ;; + "debug:android") + android_build debug + ;; + "release:android") + android_build release + ;; +esac diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 432d05136..a5c5c8c19 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache", "workspaceFolder": "/home/vscode/rustdesk", - "postStartCommand": "./entrypoint", + "postStartCommand": ".devcontainer/build", "features": { "ghcr.io/devcontainers/features/java:1": {}, "ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": { @@ -20,7 +20,9 @@ "mutantdino.resourcemonitor", "rust-lang.rust-analyzer", "tamasfe.even-better-toml", - "serayuzgur.crates" + "serayuzgur.crates", + "mhutchie.git-graph", + "eamodio.gitlens" ], "settings": { "files.watcherExclude": { diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh new file mode 100644 index 000000000..a206c3607 --- /dev/null +++ b/.devcontainer/setup.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e +case $1 in + android) + # install deps + cd $WORKDIR/flutter + flutter pub get + wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz + tar xzf so.tar.gz + rm so.tar.gz + sudo chown -R $(whoami) $ANDROID_HOME + echo "Setup is Done." + ;; + linux) + echo "Linux Setup" + ;; +esac + + \ No newline at end of file diff --git a/flutter/build_android.sh b/flutter/build_android.sh index 0a2854299..b7a475d63 100755 --- a/flutter/build_android.sh +++ b/flutter/build_android.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash + +MODE=${MODE:=debug} $ANDROID_NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* -#flutter build apk --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info -flutter build apk --split-per-abi --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info -#flutter build appbundle --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info +flutter build apk --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info +flutter build apk --split-per-abi --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info +flutter build appbundle --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info # build in linux # $ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* From 4d554044e889f27924d056a7fbadfea683d0db88 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Mon, 20 Feb 2023 15:39:46 +0000 Subject: [PATCH 133/202] fix key gen --- .devcontainer/build.sh | 9 +++++---- .devcontainer/setup.sh | 0 2 files changed, 5 insertions(+), 4 deletions(-) mode change 100644 => 100755 .devcontainer/setup.sh diff --git a/.devcontainer/build.sh b/.devcontainer/build.sh index a41d4dc38..7a85b6da6 100755 --- a/.devcontainer/build.sh +++ b/.devcontainer/build.sh @@ -31,12 +31,13 @@ key_gen(){ then if [ ! -f $HOME/upload-keystore.jks ] then - echo "Remember the password you enter in keytool!" + echo -e "\n$HOME/upload-keystore.jks is not created.\nLet's create it.\nRemember the password you enter in keytool!" keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload - else - read -r -p "enter the password used to generate $HOME/upload-keystore.jks" password - echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties fi + read -r -p "enter the password used to generate $HOME/upload-keystore.jks\n" password + echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties + else + echo "Believing storeFile is created in $WORKDIR/flutter/android/key.properties" fi } diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh old mode 100644 new mode 100755 From ededf09a67903da1cab746c384bb76d8e9a9c1d9 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Mon, 20 Feb 2023 16:31:27 +0000 Subject: [PATCH 134/202] build sh --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a5c5c8c19..cd82c75e3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache", "workspaceFolder": "/home/vscode/rustdesk", - "postStartCommand": ".devcontainer/build", + "postStartCommand": ".devcontainer/build.sh", "features": { "ghcr.io/devcontainers/features/java:1": {}, "ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": { From 8f35f5c65b80b796d8878784e815c208e1fc7efd Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Mon, 20 Feb 2023 18:05:16 +0000 Subject: [PATCH 135/202] setup key --- .devcontainer/build.sh | 7 ++++--- .devcontainer/setup.sh | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.devcontainer/build.sh b/.devcontainer/build.sh index 7a85b6da6..df87aace7 100755 --- a/.devcontainer/build.sh +++ b/.devcontainer/build.sh @@ -14,6 +14,8 @@ build(){ build_arm64(){ CWD=$(pwd) + cd $WORKDIR/flutter + flutter pub get cd $WORKDIR $WORKDIR/flutter/ndk_arm64.sh cp $WORKDIR/target/aarch64-linux-android/release/liblibrustdesk.so $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so @@ -31,13 +33,12 @@ key_gen(){ then if [ ! -f $HOME/upload-keystore.jks ] then - echo -e "\n$HOME/upload-keystore.jks is not created.\nLet's create it.\nRemember the password you enter in keytool!" - keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload + $WORKDIR/.devcontainer/setup.sh key fi read -r -p "enter the password used to generate $HOME/upload-keystore.jks\n" password echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties else - echo "Believing storeFile is created in $WORKDIR/flutter/android/key.properties" + echo "Believing storeFile is created ref: $WORKDIR/flutter/android/key.properties" fi } diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh index a206c3607..c972f47b2 100755 --- a/.devcontainer/setup.sh +++ b/.devcontainer/setup.sh @@ -14,6 +14,10 @@ case $1 in linux) echo "Linux Setup" ;; + key) + echo -e "\n$HOME/upload-keystore.jks is not created.\nLet's create it.\nRemember the password you enter in keytool!" + keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload + ;; esac \ No newline at end of file From cb744463d490d57e26c365dea01b218de43e0dc2 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 21 Feb 2023 10:42:03 +0800 Subject: [PATCH 136/202] screenshot required --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index ec23aa7a9..fea1a3672 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -44,7 +44,9 @@ body: id: screenshots attributes: label: Screenshots - description: If applicable, please add screenshots to help explain your problem + description: Please add screenshots to help explain your problem, if applicable, please upload video. + validations: + required: true - type: textarea id: context attributes: From 95a0d90891944ca209692cd34259729843117c3e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 21 Feb 2023 11:40:21 +0800 Subject: [PATCH 137/202] add FAQ --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df0ca8328..5e4c5e70d 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Yet another remote desktop software, written in Rust. Works out of the box, no c RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for help getting started. -[**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) From 2bef19c1a46c99bc6497b4c9d3bc06f8312dc2c1 Mon Sep 17 00:00:00 2001 From: Seff <46768740+seffs@users.noreply.github.com> Date: Tue, 21 Feb 2023 07:35:09 +0100 Subject: [PATCH 138/202] fix desktop entry key/values Similar to #1255 and related to #1299, running `desktop-file-validate /usr/share/applications/rustdesk.desktop` on Ubuntu 22.04 returns the following: ``` /usr/share/applications/rustdesk.desktop: error: value "1.2.0" for key "Version" in group "Desktop Entry" is not a known version /usr/share/applications/rustdesk.desktop: error: required key "Exec" in group "Desktop Action new-window" is not present ``` * "Version" refers to the Freedesktop Specification[1], not the program's one. Given that this was correctly defined in rustdesk-link.desktop, the same value should be used here too. * The new-window section is missing the `Exec` key. Ubuntu 22.04 refuses to launch from the Activities overview (apps menu) without this key. [1] https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html --- res/rustdesk.desktop | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/rustdesk.desktop b/res/rustdesk.desktop index c9cf1f254..ca1c9a9f7 100644 --- a/res/rustdesk.desktop +++ b/res/rustdesk.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=1.2.0 +Version=1.5.0 Name=RustDesk GenericName=Remote Desktop Comment=Remote Desktop @@ -16,4 +16,4 @@ X-Desktop-File-Install-Version=0.23 [Desktop Action new-window] Name=Open a New Window - +Exec=rustdesk %u From acf2dfd779749e92d3e0687fe40c8b8723dfd8a6 Mon Sep 17 00:00:00 2001 From: Seff <46768740+seffs@users.noreply.github.com> Date: Tue, 21 Feb 2023 07:40:54 +0100 Subject: [PATCH 139/202] fix: versioning --- res/rustdesk.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/rustdesk.desktop b/res/rustdesk.desktop index ca1c9a9f7..f31a16dec 100644 --- a/res/rustdesk.desktop +++ b/res/rustdesk.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=1.5.0 +Version=1.5 Name=RustDesk GenericName=Remote Desktop Comment=Remote Desktop From 1e1a544c9ec7ef93b2cd4a2041fcd245819b1357 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Tue, 21 Feb 2023 07:00:59 +0000 Subject: [PATCH 140/202] defaults to release --- flutter/build_android.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/build_android.sh b/flutter/build_android.sh index b7a475d63..c6b639f87 100755 --- a/flutter/build_android.sh +++ b/flutter/build_android.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -MODE=${MODE:=debug} +MODE=${MODE:=release} $ANDROID_NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* flutter build apk --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info flutter build apk --split-per-abi --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info From 4beacf93d71305577db319b0b0e716d80848dd0a Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 21 Feb 2023 15:07:44 +0800 Subject: [PATCH 141/202] kill check-hwcodec-config process Signed-off-by: 21pages --- Cargo.lock | 2 +- Cargo.toml | 1 - libs/hbb_common/Cargo.toml | 1 + libs/hbb_common/src/lib.rs | 1 + libs/scrap/src/common/hwcodec.rs | 38 ++++++++++++++++++++++---------- src/ipc.rs | 2 +- src/platform/macos.rs | 2 +- 7 files changed, 31 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48981e169..115845b50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2623,6 +2623,7 @@ dependencies = [ "serde_json 1.0.89", "socket2 0.3.19", "sodiumoxide", + "sysinfo", "tokio", "tokio-socks", "tokio-util", @@ -4887,7 +4888,6 @@ dependencies = [ "shutdown_hooks", "simple_rc", "sys-locale", - "sysinfo", "system_shutdown", "tao", "tray-icon", diff --git a/Cargo.toml b/Cargo.toml index 0ebe49fdf..f685e3f2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,6 @@ uuid = { version = "1.0", features = ["v4"] } clap = "3.0" rpassword = "7.0" base64 = "0.13" -sysinfo = "0.24" num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } default-net = "0.12.0" diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 0457bb19a..a125078d2 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -33,6 +33,7 @@ tokio-socks = { git = "https://github.com/open-trade/tokio-socks" } chrono = "0.4" backtrace = "0.3" libc = "0.2" +sysinfo = "0.24" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] mac_address = "1.1" diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 99cb6f408..bfb773908 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -42,6 +42,7 @@ pub use chrono; pub use libc; pub use directories_next; pub mod keyboard; +pub use sysinfo; #[cfg(feature = "quic")] pub type Stream = quic::Connection; diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 9cd6077a6..27b157b79 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -317,16 +317,30 @@ pub fn check_config() { } pub fn check_config_process(force_reset: bool) { - if force_reset { - HwCodecConfig::remove(); - } - if let Ok(exe) = std::env::current_exe() { - std::thread::spawn(move || { - std::process::Command::new(exe) - .arg("--check-hwcodec-config") - .status() - .ok(); - HwCodecConfig::refresh(); - }); - }; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; + + std::thread::spawn(move || { + if force_reset { + HwCodecConfig::remove(); + } + if let Ok(exe) = std::env::current_exe() { + if let Some(file_name) = exe.file_name().to_owned() { + let s = System::new_all(); + let arg = "--check-hwcodec-config"; + for process in s.processes_by_name(&file_name.to_string_lossy().to_string()) { + if process.cmd().iter().any(|cmd| cmd.contains(arg)) { + log::warn!("already have process {}", arg); + return; + } + } + if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() { + let second = 3; + std::thread::sleep(std::time::Duration::from_secs(second)); + // kill: Different platforms have different results + child.kill().ok(); + HwCodecConfig::refresh(); + } + } + }; + }); } diff --git a/src/ipc.rs b/src/ipc.rs index 699b0bcd7..b1b130340 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -549,7 +549,7 @@ async fn check_pid(postfix: &str) { file.read_to_string(&mut content).ok(); let pid = content.parse::().unwrap_or(0); if pid > 0 { - use sysinfo::{ProcessExt, System, SystemExt}; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; let mut sys = System::new(); sys.refresh_processes(); if let Some(p) = sys.process(pid.into()) { diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 0c8c51455..910c26982 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -558,7 +558,7 @@ pub fn hide_dock() { } fn check_main_window() -> bool { - use sysinfo::{ProcessExt, System, SystemExt}; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; let mut sys = System::new(); sys.refresh_processes(); let app = format!("/Applications/{}.app", crate::get_app_name()); From a91c9ef614036aeb3806ef3905b125a19d78f167 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 21 Feb 2023 16:29:06 +0800 Subject: [PATCH 142/202] fix ab ActionMore can't popup Signed-off-by: 21pages --- flutter/lib/common/widgets/address_book.dart | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index bd2a01296..88a5aaaa3 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -43,11 +43,8 @@ class _AddressBookState extends State { return Obx(() { if (gFFI.userModel.userName.value.isEmpty) { return Center( - child: ElevatedButton( - onPressed: loginDialog, - child: Text(translate("Login")) - ) - ); + child: ElevatedButton( + onPressed: loginDialog, child: Text(translate("Login")))); } else { if (gFFI.abModel.abLoading.value) { return const Center( @@ -153,13 +150,13 @@ class _AddressBookState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(translate('Tags')), - GestureDetector( - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; + Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; menuPos = RelativeRect.fromLTRB(x, y, x, y); }, - onTap: () => _showMenu(menuPos), + onPointerUp: (_) => _showMenu(menuPos), child: ActionMore()), ], ); From 9dbd1f88f5ec72b0c320173ff28ae7d38d2a2889 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 21 Feb 2023 18:43:43 +0800 Subject: [PATCH 143/202] listen flutter key event when there's no input monitor permission Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 11 +---------- flutter/lib/consts.dart | 1 + flutter/lib/desktop/pages/desktop_home_page.dart | 5 +++++ flutter/lib/desktop/pages/remote_tab_page.dart | 2 ++ flutter/lib/desktop/widgets/remote_menubar.dart | 6 ++++++ flutter/lib/models/input_model.dart | 4 ++++ src/flutter_ffi.rs | 9 ++++++--- 7 files changed, 25 insertions(+), 13 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 5833e760d..dd39cbdfd 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_hbb/models/state_model.dart'; -import '../../common.dart'; import '../../models/input_model.dart'; class RawKeyFocusScope extends StatelessWidget { @@ -20,13 +18,6 @@ class RawKeyFocusScope extends StatelessWidget { @override Widget build(BuildContext context) { - final FocusOnKeyCallback? onKey; - if (isAndroid) { - onKey = inputModel.handleRawKeyEvent; - } else { - onKey = stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null; - } - return FocusScope( autofocus: true, child: Focus( @@ -34,7 +25,7 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKey: onKey, + onKey: inputModel.handleRawKeyEvent, child: child)); } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 2b4bc7f32..a4cb50025 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -20,6 +20,7 @@ const String kAppTypeDesktopPortForward = "port forward"; const String kWindowMainWindowOnTop = "main_window_on_top"; const String kWindowGetWindowInfo = "get_window_info"; +const String kWindowDisableGrabKeyboard = "disable_grab_keyboard"; const String kWindowActionRebuild = "rebuild"; const String kWindowEventHide = "hide"; const String kWindowEventShow = "show"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index b5cadbcdf..ff99c9dc8 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -14,6 +14,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -498,6 +499,10 @@ class _DesktopHomePageState extends State if (watchIsInputMonitoring) { if (bind.mainIsCanInputMonitoring(prompt: false)) { watchIsInputMonitoring = false; + // Do not notify for now. + // Monitoring may not take effect until the process is restarted. + // rustDeskWinManager.call( + // WindowType.RemoteDesktop, kWindowDisableGrabKeyboard, ''); setState(() {}); } } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 64c78f24d..ef3a0dd04 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -111,6 +111,8 @@ class _ConnectionTabPageState extends State { forceRelay: args['forceRelay'], ), )); + } else if (call.method == kWindowDisableGrabKeyboard) { + stateGlobal.grabKeyboard = false; } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index e82e9d26e..adbf50abe 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -650,6 +650,12 @@ class _RemoteMenubarState extends State { } Widget _buildKeyboard(BuildContext context) { + // Do not support peer 1.1.9. + if (Platform.isMacOS && stateGlobal.grabKeyboard) { + bind.sessionSetKeyboardMode(id: widget.id, value: 'map'); + return Offstage(); + } + FfiModel ffiModel = Provider.of(context); if (ffiModel.permissions['keyboard'] == false) { return Offstage(); diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index b1491d526..9a5b06b14 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -58,6 +58,10 @@ class InputModel { InputModel(this.parent); KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { + if (!stateGlobal.grabKeyboard) { + return KeyEventResult.handled; + } + // * Currently mobile does not enable map mode if (isDesktop) { bind.sessionGetKeyboardMode(id: id).then((result) { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f3bc45856..68ddce9b7 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,13 +1,13 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::get_default_sound_input; use crate::{ client::file_trait::FileManager, - common::make_fd_to_json, common::is_keyboard_mode_supported, + common::make_fd_to_json, flutter::{self, SESSIONS}, flutter::{session_add, session_start_}, ui_interface::{self, *}, }; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::get_default_sound_input; use flutter_rust_bridge::{StreamSink, SyncReturn}; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, @@ -1181,6 +1181,9 @@ pub fn main_start_grab_keyboard() -> SyncReturn { return SyncReturn(false); } crate::keyboard::client::start_grab_loop(); + if !is_can_input_monitoring(false) { + return SyncReturn(false); + } SyncReturn(true) } From ac6ea0d9fc13c1cdfbfd7c49c2b0e76c13568012 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 21 Feb 2023 19:04:22 +0800 Subject: [PATCH 144/202] trivial changes Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index adbf50abe..45857aa45 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -651,7 +651,7 @@ class _RemoteMenubarState extends State { Widget _buildKeyboard(BuildContext context) { // Do not support peer 1.1.9. - if (Platform.isMacOS && stateGlobal.grabKeyboard) { + if (stateGlobal.grabKeyboard) { bind.sessionSetKeyboardMode(id: widget.id, value: 'map'); return Offstage(); } From bfb0ea9d1dc36afa9e973060590ed46bd7dd85d2 Mon Sep 17 00:00:00 2001 From: Integral <71180087+Integral-Tech@users.noreply.github.com> Date: Tue, 21 Feb 2023 20:50:59 +0800 Subject: [PATCH 145/202] Update cn.rs --- src/lang/cn.rs | 138 ++++++++++++++++++++++++------------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 9d0d176da..78a1f9e73 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "状态"), ("Your Desktop", "你的桌面"), - ("desk_tip", "你的桌面可以通过下面的ID和密码访问。"), + ("desk_tip", "你的桌面可以通过下面的 ID 和密码访问。"), ("Password", "密码"), ("Ready", "就绪"), ("Established", "已建立"), @@ -11,7 +11,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Service", "允许服务"), ("Start Service", "启动服务"), ("Service is running", "服务正在运行"), - ("Service is not running", "服务没有启动"), + ("Service is not running", "服务未运行"), ("not_ready_status", "未就绪,请检查网络连接"), ("Control Remote Desktop", "控制远程桌面"), ("Transfer File", "传输文件"), @@ -19,49 +19,49 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recent Sessions", "最近访问过"), ("Address Book", "地址簿"), ("Confirmation", "确认"), - ("TCP Tunneling", "TCP隧道"), + ("TCP Tunneling", "TCP 隧道"), ("Remove", "删除"), ("Refresh random password", "刷新随机密码"), ("Set your own password", "设置密码"), ("Enable Keyboard/Mouse", "允许控制键盘/鼠标"), ("Enable Clipboard", "允许同步剪贴板"), ("Enable File Transfer", "允许传输文件"), - ("Enable TCP Tunneling", "允许建立TCP隧道"), - ("IP Whitelisting", "IP白名单"), + ("Enable TCP Tunneling", "允许建立 TCP 隧道"), + ("IP Whitelisting", "IP 白名单"), ("ID/Relay Server", "ID/中继服务器"), ("Import Server Config", "导入服务器配置"), ("Export Server Config", "导出服务器配置"), ("Import server configuration successfully", "导入服务器配置信息成功"), ("Export server configuration successfully", "导出服务器配置信息成功"), - ("Invalid server configuration", "无效服务器配置,请修改后重新拷贝配置信息到剪贴板后点击此按钮"), - ("Clipboard is empty", "拷贝配置信息到剪贴板后点击此按钮,可以自动导入配置"), + ("Invalid server configuration", "服务器配置无效,请修改后重新复制配置信息到剪贴板,然后点击此按钮"), + ("Clipboard is empty", "复制配置信息到剪贴板后点击此按钮,可以自动导入配置"), ("Stop service", "停止服务"), - ("Change ID", "改变ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), - ("id_change_tip", "只可以使用字母a-z, A-Z, 0-9, _ (下划线)。首字母必须是a-z, A-Z。长度在6与16之间。"), + ("Change ID", "更改 ID"), + ("Your new ID", "你的新 ID"), + ("length %min% to %max%", "长度在 %min 与 %max 之间"), + ("starts with a letter", "以字母开头"), + ("allowed characters", "使用允许的字符"), + ("id_change_tip", "只可以使用字母 a-z, A-Z, 0-9, _ (下划线)。首字母必须是 a-z, A-Z。长度在 6 与 16 之间。"), ("Website", "网站"), ("About", "关于"), ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Privacy Statement", "隐私声明"), ("Mute", "静音"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "构建日期"), + ("Version", "版本"), + ("Home", "主页"), ("Audio Input", "音频输入"), ("Enhancements", "增强功能"), ("Hardware Codec", "硬件编解码"), ("Adaptive Bitrate", "自适应码率"), - ("ID Server", "ID服务器"), + ("ID Server", "ID 服务器"), ("Relay Server", "中继服务器"), - ("API Server", "API服务器"), - ("invalid_http", "必须以http://或者https://开头"), - ("Invalid IP", "无效IP"), + ("API Server", "API 服务器"), + ("invalid_http", "必须以 http:// 或者 https:// 开头"), + ("Invalid IP", "无效 IP"), ("Invalid format", "无效格式"), ("server_not_support", "服务器暂不支持"), - ("Not available", "已被占用"), + ("Not available", "不可用"), ("Too frequent", "修改太频繁,请稍后再试"), ("Cancel", "取消"), ("Skip", "跳过"), @@ -72,12 +72,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter your password", "请输入密码"), ("Remember password", "记住密码"), ("Wrong Password", "密码错误"), - ("Do you want to enter again?", "还想输入一次吗?"), + ("Do you want to enter again?", "是否要再次输入?"), ("Connection Error", "连接错误"), ("Error", "错误"), ("Reset by the peer", "连接被对方关闭"), ("Connecting...", "正在连接..."), - ("Connection in progress. Please wait.", "连接进行中,请稍等。"), + ("Connection in progress. Please wait.", "正在进行连接,请稍候。"), ("Please try 1 minute later", "一分钟后再试"), ("Login Error", "登录错误"), ("Successful", "成功"), @@ -102,14 +102,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unselect All", "取消全选"), ("Empty Directory", "空文件夹"), ("Not an empty directory", "这不是一个空文件夹"), - ("Are you sure you want to delete this file?", "是否删除此文件?"), - ("Are you sure you want to delete this empty directory?", "是否删除此空文件夹?"), - ("Are you sure you want to delete the file of this directory?", "是否删除文件夹下的文件?"), + ("Are you sure you want to delete this file?", "是否删除此文件?"), + ("Are you sure you want to delete this empty directory?", "是否删除此空文件夹?"), + ("Are you sure you want to delete the file of this directory?", "是否删除此文件夹下的文件?"), ("Do this for all conflicts", "应用于其它冲突"), ("This is irreversible!", "此操作不可逆!"), ("Deleting", "正在删除"), ("files", "文件"), - ("Waiting", "等待..."), + ("Waiting", "正在等待..."), ("Finished", "完成"), ("Speed", "速度"), ("Custom Image Quality", "设置画面质量"), @@ -128,31 +128,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", "自定义"), ("Show remote cursor", "显示远程光标"), ("Show quality monitor", "显示质量监测"), - ("Disable clipboard", "禁止剪贴板"), - ("Lock after session end", "断开后锁定远程电脑"), + ("Disable clipboard", "禁用剪贴板"), + ("Lock after session end", "会话结束后锁定远程电脑"), ("Insert", "插入"), ("Insert Lock", "锁定远程电脑"), ("Refresh", "刷新画面"), - ("ID does not exist", "ID不存在"), + ("ID does not exist", "ID 不存在"), ("Failed to connect to rendezvous server", "连接注册服务器失败"), ("Please try later", "请稍后再试"), - ("Remote desktop is offline", "远程电脑不在线"), - ("Key mismatch", "Key不匹配"), + ("Remote desktop is offline", "远程电脑处于离线状态"), + ("Key mismatch", "密钥不匹配"), ("Timeout", "连接超时"), ("Failed to connect to relay server", "无法连接到中继服务器"), ("Failed to connect via rendezvous server", "无法通过注册服务器建立连接"), ("Failed to connect via relay server", "无法通过中继服务器建立连接"), - ("Failed to make direct connection to remote desktop", "无法建立直接连接"), + ("Failed to make direct connection to remote desktop", "无法直接连接到远程桌面"), ("Set Password", "设置密码"), ("OS Password", "操作系统密码"), - ("install_tip", "你正在运行未安装版本,由于UAC限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将 RustDesk 安装到系统,从而规避上述问题。"), + ("install_tip", "你正在运行未安装版本,由于 UAC 限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将 RustDesk 安装到系统,从而规避上述问题。"), ("Click to upgrade", "点击这里升级"), ("Click to download", "点击这里下载"), ("Click to update", "点击这里更新"), ("Configure", "配置"), ("config_acc", "为了能够远程控制你的桌面, 请给予 RustDesk \"辅助功能\" 权限。"), ("config_screen", "为了能够远程访问你的桌面, 请给予 RustDesk \"屏幕录制\" 权限。"), - ("Installing ...", "安装 ..."), + ("Installing ...", "安装中..."), ("Install", "安装"), ("Installation", "安装"), ("Installation Path", "安装路径"), @@ -161,10 +161,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "开始安装即表示接受许可协议。"), ("Accept and Install", "同意并安装"), ("End-user license agreement", "用户协议"), - ("Generating ...", "正在产生 ..."), + ("Generating ...", "正在生成..."), ("Your installation is lower version.", "你安装的版本比当前运行的低。"), ("not_close_tcp_tip", "请在使用隧道的时候,不要关闭本窗口"), - ("Listening ...", "正在等待隧道连接 ..."), + ("Listening ...", "正在等待隧道连接..."), ("Remote Host", "远程主机"), ("Remote Port", "远程端口"), ("Action", "动作"), @@ -173,7 +173,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Address", "当前地址"), ("Change Local Port", "修改本地端口"), ("setup_server_tip", "如果需要更快连接速度,你可以选择自建服务器"), - ("Too short, at least 6 characters.", "太短了,至少6个字符"), + ("Too short, at least 6 characters.", "太短了,至少 6 个字符"), ("The confirmation is not identical.", "两次输入不匹配"), ("Permissions", "权限"), ("Accept", "接受"), @@ -183,21 +183,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Allow using clipboard", "允许使用剪贴板"), ("Allow hearing sound", "允许听到声音"), ("Allow file copy and paste", "允许复制粘贴文件"), - ("Connected", "已经连接"), + ("Connected", "已连接"), ("Direct and encrypted connection", "加密直连"), ("Relayed and encrypted connection", "加密中继连接"), ("Direct and unencrypted connection", "非加密直连"), ("Relayed and unencrypted connection", "非加密中继连接"), - ("Enter Remote ID", "输入对方ID"), + ("Enter Remote ID", "输入对方 ID"), ("Enter your password", "输入密码"), ("Logging in...", "正在登录..."), - ("Enable RDP session sharing", "允许RDP会话共享"), + ("Enable RDP session sharing", "允许 RDP 会话共享"), ("Auto Login", "自动登录(设置断开后锁定才有效)"), - ("Enable Direct IP Access", "允许IP直接访问"), - ("Rename", "改名"), + ("Enable Direct IP Access", "允许 IP 直接访问"), + ("Rename", "重命名"), ("Space", "空格"), ("Create Desktop Shortcut", "创建桌面快捷方式"), - ("Change Path", "改变路径"), + ("Change Path", "更改路径"), ("Create Folder", "创建文件夹"), ("Please enter the folder name", "请输入文件夹名称"), ("Fix it", "修复"), @@ -212,29 +212,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid port", "无效端口"), ("Closed manually by the peer", "被对方手动关闭"), ("Enable remote configuration modification", "允许远程修改配置"), - ("Run without install", "无安装运行"), + ("Run without install", "不安装直接运行"), ("Connect via relay", "中继连接"), ("Always connect via relay", "强制走中继连接"), - ("whitelist_tip", "只有白名单里的ip才能访问我"), + ("whitelist_tip", "只有白名单里的 IP 才能访问我"), ("Login", "登录"), ("Verify", "验证"), ("Remember me", "记住我"), ("Trust this device", "信任此设备"), ("Verification code", "验证码"), - ("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,输入验证码继续登录"), + ("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,请输入验证码继续登录"), ("Logout", "登出"), ("Tags", "标签"), - ("Search ID", "查找ID"), + ("Search ID", "查找 ID"), ("whitelist_sep", "可以使用逗号,分号,空格或者换行符作为分隔符"), - ("Add ID", "增加ID"), + ("Add ID", "增加 ID"), ("Add Tag", "增加标签"), ("Unselect all tags", "取消选择所有标签"), ("Network error", "网络错误"), ("Username missed", "用户名没有填写"), ("Password missed", "密码没有填写"), - ("Wrong credentials", "提供的登入信息错误"), + ("Wrong credentials", "提供的登录信息错误"), ("Edit Tag", "修改标签"), - ("Unremember Password", "忘掉密码"), + ("Unremember Password", "忘记密码"), ("Favorites", "收藏"), ("Add to Favorites", "加入到收藏"), ("Remove from Favorites", "从收藏中删除"), @@ -244,9 +244,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hostname", "主机名"), ("Discovered", "已发现"), ("install_daemon_tip", "为了开机启动,请安装系统服务。"), - ("Remote ID", "远程ID"), + ("Remote ID", "远程 ID"), ("Paste", "粘贴"), - ("Paste here?", "粘贴到这里?"), + ("Paste here?", "粘贴到这里?"), ("Are you sure to close the connection?", "是否确认关闭连接?"), ("Download new version", "下载新版本"), ("Touch mode", "触屏模式"), @@ -284,7 +284,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "是否接受?"), ("Open System Setting", "打开系统设置"), ("How to get Android input permission?", "如何获取安卓的输入权限?"), - ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允許 RustDesk 使用\"无障碍\"服务。"), + ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允许 RustDesk 使用\"无障碍\"服务。"), ("android_input_permission_tip2", "请在接下来的系统设置页面里,找到并进入 [已安装的服务] 页面,将 [RustDesk Input] 服务开启。"), ("android_new_connection_tip", "收到新的连接控制请求,对方想要控制你当前的设备。"), ("android_service_will_start_tip", "开启录屏权限将自动开启服务,允许其他设备向此设备请求建立连接。"), @@ -293,7 +293,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_start_service_tip", "点击 [启动服务] 或打开 [屏幕录制] 权限开启手机屏幕共享服务。"), ("Account", "账户"), ("Overwrite", "覆盖"), - ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), + ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), ("Quit", "退出"), ("doc_mac_permission", "https://rustdesk.com/docs/zh-cn/manual/mac#%E5%90%AF%E7%94%A8%E6%9D%83%E9%99%90"), ("Help", "帮助"), @@ -314,7 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的 RustDesk 应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), - ("Map mode", "1:1传输"), + ("Map mode", "1:1 传输"), ("Translate mode", "翻译模式"), ("Use permanent password", "使用固定密码"), ("Use both passwords", "同时使用两种密码"), @@ -355,16 +355,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Audio", "允许传输音频"), ("Unlock Network Settings", "解锁网络设置"), ("Server", "服务器"), - ("Direct IP Access", "IP直接访问"), + ("Direct IP Access", "IP 直接访问"), ("Proxy", "代理"), ("Apply", "应用"), - ("Disconnect all devices?", "断开所有远程连接?"), + ("Disconnect all devices?", "断开所有远程连接?"), ("Clear", "清空"), ("Audio Input Device", "音频输入设备"), ("Deny remote access", "拒绝远程访问"), - ("Use IP Whitelisting", "只允许白名单上的IP访问"), + ("Use IP Whitelisting", "只允许白名单上的 IP 访问"), ("Network", "网络"), - ("Enable RDP", "允许RDP访问"), + ("Enable RDP", "允许 RDP 访问"), ("Pin menubar", "固定菜单栏"), ("Unpin menubar", "取消固定菜单栏"), ("Recording", "录屏"), @@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "拒绝局域网发现"), ("Write a message", "输入聊天消息"), ("Prompt", "提示"), - ("Please wait for confirmation of UAC...", "请等待对方确认 UAC ..."), + ("Please wait for confirmation of UAC...", "请等待对方确认 UAC..."), ("elevated_foreground_window_tip", "远端桌面的当前窗口需要更高的权限才能操作, 暂时无法使用鼠标键盘, 可以请求对方最小化当前窗口, 或者在连接管理窗口点击提升。为避免这个问题,建议在远端设备上安装本软件。"), ("Disconnected", "会话已结束"), ("Other", "其他"), @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "请求访问你的设备"), ("Hide connection management window", "隐藏连接管理窗口"), ("hide_cm_tip", "在只允许密码连接并且只用固定密码的情况下才允许隐藏"), - ("wayland_experiment_tip", "Wayland 支持处于实验阶段,如果你需要使用无人值守访问,请使用X11。"), + ("wayland_experiment_tip", "Wayland 支持处于实验阶段,如果你需要使用无人值守访问,请使用 X11。"), ("Right click to select tabs", "右键选择选项卡"), ("Skipped", "已跳过"), ("Add to Address Book", "添加到地址簿"), @@ -417,7 +417,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local keyboard type", "本地键盘类型"), ("Select local keyboard type", "请选择本地键盘类型"), ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装 nouveau 驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), - ("Always use software rendering", "使用软件渲染"), + ("Always use software rendering", "始终使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), ("config_microphone", "为了支持通过麦克风进行音频传输,请给予 RustDesk \"录音\"权限。"), ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), @@ -434,25 +434,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("lowercase", "小写字母"), ("digit", "数字"), ("special character", "特殊字符"), - ("length>=8", "长度不小于8"), + ("length>=8", "长度不小于 8"), ("Weak", "弱"), ("Medium", "中"), ("Strong", "强"), ("Switch Sides", "反转访问方向"), - ("Please confirm if you want to share your desktop?", "请确认要让对方访问你的桌面?"), + ("Please confirm if you want to share your desktop?", "请确认是否要让对方访问你的桌面?"), ("Display", "显示"), ("Default View Style", "默认显示方式"), ("Default Scroll Style", "默认滚动方式"), ("Default Image Quality", "默认图像质量"), ("Default Codec", "默认编解码"), - ("Bitrate", "波特率"), + ("Bitrate", "比特率"), ("FPS", "帧率"), ("Auto", "自动"), ("Other Default Options", "其它默认选项"), ("Voice call", "语音通话"), ("Text chat", "文字聊天"), - ("Stop voice call", "停止语音聊天"), - ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在ID后面添加/r,或者在卡片选项里选择强制走中继连接。"), + ("Stop voice call", "停止语音通话"), + ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在 ID 后面添加/r,或者在卡片选项里选择强制走中继连接。"), ("Reconnect", "重连"), ].iter().cloned().collect(); } From c1066aab3a344430d86010eeae6259e6b84ce183 Mon Sep 17 00:00:00 2001 From: Integral <71180087+Integral-Tech@users.noreply.github.com> Date: Tue, 21 Feb 2023 21:42:15 +0800 Subject: [PATCH 146/202] Update cn.rs Some small tweaks --- src/lang/cn.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 78a1f9e73..4824ac5e9 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -122,9 +122,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stretch", "伸展"), ("Scrollbar", "滚动条"), ("ScrollAuto", "自动滚动"), - ("Good image quality", "好画质"), - ("Balanced", "一般画质"), - ("Optimize reaction time", "优化反应时间"), + ("Good image quality", "画质最优化"), + ("Balanced", "平衡"), + ("Optimize reaction time", "速度最优化"), ("Custom", "自定义"), ("Show remote cursor", "显示远程光标"), ("Show quality monitor", "显示质量监测"), @@ -215,7 +215,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Run without install", "不安装直接运行"), ("Connect via relay", "中继连接"), ("Always connect via relay", "强制走中继连接"), - ("whitelist_tip", "只有白名单里的 IP 才能访问我"), + ("whitelist_tip", "只有白名单里的 IP 才能访问本机"), ("Login", "登录"), ("Verify", "验证"), ("Remember me", "记住我"), @@ -396,7 +396,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "或"), ("Continue with", "使用"), ("Elevate", "提权"), - ("Zoom cursor", "缩放鼠标"), + ("Zoom cursor", "缩放光标"), ("Accept sessions via password", "只允许密码访问"), ("Accept sessions via click", "只允许点击访问"), ("Accept sessions via both", "允许密码或点击访问"), @@ -445,7 +445,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Default Scroll Style", "默认滚动方式"), ("Default Image Quality", "默认图像质量"), ("Default Codec", "默认编解码"), - ("Bitrate", "比特率"), + ("Bitrate", "码率"), ("FPS", "帧率"), ("Auto", "自动"), ("Other Default Options", "其它默认选项"), From f03c265f9c224b95c169b34447ff2bc69707458d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 21 Feb 2023 21:39:32 +0800 Subject: [PATCH 147/202] fix: orderout not working when fullscreen on macos --- flutter/lib/desktop/widgets/tabbar_widget.dart | 15 +++++++++++---- flutter/pubspec.yaml | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 9ba7a6315..357abab2e 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -548,13 +548,20 @@ class WindowActionPanelState extends State if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) { await rustDeskWinManager.unregisterActiveWindow(kMainWindowId); } - // `hide` must be placed after unregisterActiveWindow, because once all windows are hidden, - // flutter closes the application on macOS. We should ensure the post-run logic has ran successfully. - // e.g.: saving window position. + // macOS specific workaround, the windows is not hiding when in fullscreen. + if (Platform.isMacOS && await windowManager.isFullScreen()) { + await windowManager.setFullScreen(false); + await Future.delayed(Duration(seconds: 1)); + } await windowManager.hide(); } else { // it's safe to hide the subwindow - await WindowController.fromWindowId(kWindowId!).hide(); + final controller = WindowController.fromWindowId(kWindowId!); + if (Platform.isMacOS && await controller.isFullScreen()) { + await controller.setFullscreen(false); + await Future.delayed(Duration(seconds: 1)); + } + await controller.hide(); await Future.wait([ rustDeskWinManager .call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}), diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index df29252c9..a4584f4a1 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -59,7 +59,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 + ref: 84a027ac2eed31e1b7c0ad11de47ed846501824e freezed_annotation: ^2.0.3 flutter_custom_cursor: ^0.0.4 window_size: From a46c39a67b78ab02cb2be6d049947a29112f8ea4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 22 Feb 2023 09:04:43 +0800 Subject: [PATCH 148/202] add: texture renderer --- flutter/lib/desktop/widgets/tabbar_widget.dart | 2 +- flutter/pubspec.yaml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 357abab2e..ee3aaaf2c 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -548,7 +548,7 @@ class WindowActionPanelState extends State if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) { await rustDeskWinManager.unregisterActiveWindow(kMainWindowId); } - // macOS specific workaround, the windows is not hiding when in fullscreen. + // macOS specific workaround, the window is not hiding when in fullscreen. if (Platform.isMacOS && await windowManager.isFullScreen()) { await windowManager.setFullScreen(false); await Future.delayed(Duration(seconds: 1)); diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a4584f4a1..e009ea890 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -59,7 +59,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 84a027ac2eed31e1b7c0ad11de47ed846501824e + ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a freezed_annotation: ^2.0.3 flutter_custom_cursor: ^0.0.4 window_size: @@ -92,6 +92,7 @@ dependencies: password_strength: ^0.2.0 flutter_launcher_icons: ^0.11.0 flutter_keyboard_visibility: ^5.4.0 + texture_rgba_renderer: ^0.0.8 dev_dependencies: From ead828071fb79449de1d71aa15e4f2e6fb432021 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Wed, 22 Feb 2023 10:04:49 +0530 Subject: [PATCH 149/202] dev container spin up --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 5e4c5e70d..a1790107f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,13 @@ Yet another remote desktop software, written in Rust. Works out of the box, no c RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for help getting started. [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) +## Dev Container + +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk) + +If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. + +[**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) From db5a8404fec93f9817355639b760088bc712a3d7 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Wed, 22 Feb 2023 10:30:18 +0530 Subject: [PATCH 150/202] devcontainer docs --- README.md | 12 +++++++----- docs/DEVCONTAINER.md | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 docs/DEVCONTAINER.md diff --git a/README.md b/README.md index a1790107f..c081ca9cf 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,6 @@ Yet another remote desktop software, written in Rust. Works out of the box, no c RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for help getting started. [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) -## Dev Container - -[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk) - -If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. [**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) @@ -48,6 +43,13 @@ Below are the servers you are using for free, they may change over time. If you | USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | | Ukraine (Kyiv) | dc.volia (2VM) | 2 vCPU / 4GB RAM | +## Dev Container + +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk) + +If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. + +Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info. ## Dependencies Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only. diff --git a/docs/DEVCONTAINER.md b/docs/DEVCONTAINER.md new file mode 100644 index 000000000..067e0ecf9 --- /dev/null +++ b/docs/DEVCONTAINER.md @@ -0,0 +1,14 @@ + +After the start of devcontainer in docker container, a linux binary in debug mode is created. + +Currently devcontainer offers linux and android builds in both debug and release mode. + +Below is the table on commands to run from root of the project for creating specific builds. + +Command|Build Type|Mode +-|-|-| +`.devcontainer/build.sh --debug linux`|Linux|debug +`.devcontainer/build.sh --release linux`|Linux|release +`.devcontainer/build.sh --debug android`|android-arm64|debug +`.devcontainer/build.sh --release android`|android-arm64|debug + From 9873a2d70032e63d46b760486190c4cad9f531d5 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Wed, 22 Feb 2023 10:54:16 +0530 Subject: [PATCH 151/202] Don't run github actions on ignored paths. --- .github/workflows/ci.yml | 5 +++++ .github/workflows/flutter-ci.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e1702a60..bba114315 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,9 @@ name: CI on: workflow_dispatch: pull_request: + paths-ignore: + - "docs/**" + - "README.md" push: branches: - master @@ -14,6 +17,8 @@ on: - '*' paths-ignore: - ".github/**" + - "docs/**" + - "README.md" jobs: # ensure_cargo_fmt: diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 78c60df37..2386f17dd 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -3,6 +3,9 @@ name: Full Flutter CI on: workflow_dispatch: pull_request: + paths-ignore: + - "docs/**" + - "README.md" push: branches: - master @@ -10,6 +13,8 @@ on: - '*' paths-ignore: - ".github/**" + - "docs/**" + - "README.md" env: LLVM_VERSION: "15.0.6" From 65374b25933adb74f19a99fdd2864788ecddbf30 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 22 Feb 2023 13:31:09 +0800 Subject: [PATCH 152/202] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index c081ca9cf..419a91f96 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,6 @@ RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/ [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) -[**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) - [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) [**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) From c26c3459058302b305022a58b632e7f33349ed6d Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 22 Feb 2023 13:31:52 +0800 Subject: [PATCH 153/202] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 419a91f96..8af79915b 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Below are the servers you are using for free, they may change over time. If you If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info. + ## Dependencies Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only. From 848872e914af67368636c458d8abfee324fe472b Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Wed, 22 Feb 2023 14:35:26 +0330 Subject: [PATCH 154/202] Update fa.rs --- src/lang/fa.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 00f6b70ac..70051f3e8 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -442,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), ("Display", "نمایش دادن"), ("Default View Style", "سبک نمایش پیش فرض"), - ("Default Scroll Style", "سبک پیش‌فرض اسکرول"), + ("Default Scroll Style", "سبک پیش‌ فرض اسکرول"), ("Default Image Quality", "کیفیت تصویر پیش فرض"), ("Default Codec", "کدک پیش فرض"), ("Bitrate", "میزان بیت صفحه نمایش"), @@ -452,7 +452,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "تماس صوتی"), ("Text chat", "گفتگو متنی (چت متنی)"), ("Stop voice call", "توقف تماس صوتی"), - ("relay_hint_tip", ""), - ("Reconnect", ""), + ("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), + ("Reconnect", "اتصال مجدد"), ].iter().cloned().collect(); } From 325077435c6a0e82c2109f30d7b2fecdcfb40165 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 22 Feb 2023 22:13:21 +0100 Subject: [PATCH 155/202] file manager redesign implementation --- flutter/lib/common.dart | 20 +- flutter/lib/consts.dart | 2 +- .../lib/desktop/pages/file_manager_page.dart | 1024 ++++++++++------- .../desktop/pages/file_manager_tab_page.dart | 20 +- flutter/lib/desktop/widgets/menu_button.dart | 6 +- flutter/pubspec.lock | 10 +- flutter/pubspec.yaml | 1 + 7 files changed, 652 insertions(+), 431 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index e1dd1a1f8..ff8dfbb09 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -152,7 +152,7 @@ class MyTheme { static const Color canvasColor = Color(0xFF212121); static const Color border = Color(0xFFCCCCCC); static const Color idColor = Color(0xFF00B6F0); - static const Color darkGray = Color(0xFFB9BABC); + static const Color darkGray = Color.fromARGB(255, 148, 148, 148); static const Color cmIdColor = Color(0xFF21790B); static const Color dark = Colors.black87; static const Color button = Color(0xFF2C8CFF); @@ -160,8 +160,9 @@ class MyTheme { static ThemeData lightTheme = ThemeData( brightness: Brightness.light, - backgroundColor: Color(0xFFFFFFFF), - scaffoldBackgroundColor: Color(0xFFEEEEEE), + backgroundColor: Color(0xFFEEEEEE), + hoverColor: Color.fromARGB(255, 224, 224, 224), + scaffoldBackgroundColor: Color(0xFFFFFFFF), textTheme: const TextTheme( titleLarge: TextStyle(fontSize: 19, color: Colors.black87), titleSmall: TextStyle(fontSize: 14, color: Colors.black87), @@ -169,6 +170,7 @@ class MyTheme { bodyMedium: TextStyle(fontSize: 14, color: Colors.black87, height: 1.25), labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)), + cardColor: Color(0xFFEEEEEE), hintColor: Color(0xFFAAAAAA), primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, @@ -191,8 +193,9 @@ class MyTheme { ); static ThemeData darkTheme = ThemeData( brightness: Brightness.dark, - backgroundColor: Color(0xFF252525), - scaffoldBackgroundColor: Color(0xFF141414), + backgroundColor: Color(0xFF24252B), + hoverColor: Color.fromARGB(255, 45, 46, 53), + scaffoldBackgroundColor: Color(0xFF18191E), textTheme: const TextTheme( titleLarge: TextStyle(fontSize: 19), titleSmall: TextStyle(fontSize: 14), @@ -200,7 +203,7 @@ class MyTheme { bodyMedium: TextStyle(fontSize: 14, height: 1.25), labelLarge: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, color: accent80)), - cardColor: Color(0xFF252525), + cardColor: Color(0xFF24252B), primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( @@ -217,9 +220,8 @@ class MyTheme { style: ButtonStyle(splashFactory: NoSplash.splashFactory), ) : null, - checkboxTheme: const CheckboxThemeData( - checkColor: MaterialStatePropertyAll(dark) - ), + checkboxTheme: + const CheckboxThemeData(checkColor: MaterialStatePropertyAll(dark)), ).copyWith( extensions: >[ ColorThemeExtension.dark, diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 2b4bc7f32..22ba221a9 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -52,7 +52,7 @@ const int kDesktopMaxDisplayHeight = 1080; const double kDesktopFileTransferNameColWidth = 200; const double kDesktopFileTransferModifiedColWidth = 120; -const double kDesktopFileTransferRowHeight = 25.0; +const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferHeaderHeight = 25.0; // https://en.wikipedia.org/wiki/Non-breaking_space diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 4edffb3b6..262121f3d 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -2,20 +2,23 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:percent_indicator/percent_indicator.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_hbb/desktop/widgets/list_search_action_listener.dart'; +import 'package:flutter_hbb/desktop/widgets/menu_button.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/file_model.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; + import '../../consts.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; - import '../../common.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; @@ -147,7 +150,7 @@ class _FileManagerPageState extends State value: _ffi.fileModel, child: Consumer(builder: (context, model, child) { return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Row( children: [ Flexible(flex: 3, child: body(isLocal: true)), @@ -192,35 +195,42 @@ class _FileManagerPageState extends State ]; return Listener( - onPointerDown: (e) { - final x = e.position.dx; - final y = e.position.dy; - menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - child: IconButton( - icon: const Icon(Icons.more_vert), - splashRadius: kDesktopIconButtonSplashRadius, - onPressed: () => mod_menu.showMenu( - context: context, - position: menuPos, - items: items - .map((e) => e.build( - context, - MenuConfig( - commonColor: CustomPopupMenuTheme.commonColor, - height: CustomPopupMenuTheme.height, - dividerHeight: CustomPopupMenuTheme.dividerHeight))) - .expand((i) => i) - .toList(), - elevation: 8, - ), - )); + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; + menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + child: MenuButton( + onPressed: () => mod_menu.showMenu( + context: context, + position: menuPos, + items: items + .map( + (e) => e.build( + context, + MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight), + ), + ) + .expand((i) => i) + .toList(), + elevation: 8, + ), + child: SvgPicture.asset( + "assets/dots.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + ); } Widget body({bool isLocal = false}) { final scrollController = ScrollController(); return Container( - decoration: BoxDecoration(border: Border.all(color: Colors.black26)), margin: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(8.0), child: DropTarget( @@ -231,18 +241,22 @@ class _FileManagerPageState extends State onDragExited: (exit) { _dropMaskVisible.value = false; }, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - headTools(isLocal), - Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + headTools(isLocal), + Expanded( child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: _buildFileList(context, isLocal, scrollController), - ) - ], - )), - ]), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildFileList(context, isLocal, scrollController), + ) + ], + ), + ), + ], + ), ), ); } @@ -295,8 +309,7 @@ class _FileManagerPageState extends State }); return; } - _jumpToEntry( - isLocal, searchResult.first, scrollController, + _jumpToEntry(isLocal, searchResult.first, scrollController, kDesktopFileTransferRowHeight, buffer); }, onSearch: (buffer) { @@ -311,8 +324,7 @@ class _FileManagerPageState extends State }); return; } - _jumpToEntry( - isLocal, searchResult.first, scrollController, + _jumpToEntry(isLocal, searchResult.first, scrollController, kDesktopFileTransferRowHeight, buffer); }, child: ObxValue( @@ -323,100 +335,118 @@ class _FileManagerPageState extends State }).toList(growable: false) : entries; final rows = filteredEntries.map((entry) { - final sizeStr = - entry.isFile ? readableFileSize(entry.size.toDouble()) : ""; - final lastModifiedStr = entry.isDrive - ? " " - : "${entry.lastModified().toString().replaceAll(".000", "")} "; + final sizeStr = + entry.isFile ? readableFileSize(entry.size.toDouble()) : ""; + final lastModifiedStr = entry.isDrive + ? " " + : "${entry.lastModified().toString().replaceAll(".000", "")} "; final isSelected = selectedEntries.contains(entry); - return SizedBox( - key: ValueKey(entry.name), - height: kDesktopFileTransferRowHeight, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - const Divider( - height: 1, - ), - Expanded( - child: Ink( - decoration: isSelected - ? BoxDecoration(color: Theme.of(context).hoverColor) - : null, - child: InkWell( - child: Row(children: [ - GestureDetector( - child: Container( - width: kDesktopFileTransferNameColWidth, - child: Tooltip( - waitDuration: Duration(milliseconds: 500), - message: entry.name, - child: Row(children: [ - entry.isDrive - ? Image( - image: iconHardDrive, - fit: BoxFit.scaleDown, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7)) - .paddingAll(4) - : Icon( - entry.isFile - ? Icons.feed_outlined - : Icons.folder, - size: 20, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7), - ).marginSymmetric(horizontal: 2), - Expanded( - child: Text(entry.name.nonBreaking, - overflow: TextOverflow.ellipsis)) - ]), - )), - onTap: () { - final items = getSelectedItems(isLocal); - // handle double click - if (_checkDoubleClick(entry)) { - openDirectory(entry.path, isLocal: isLocal); - items.clear(); - return; - } - _onSelectedChanged( - items, filteredEntries, entry, isLocal); - }, - ), - GestureDetector( - child: SizedBox( - width: kDesktopFileTransferModifiedColWidth, - child: Tooltip( - waitDuration: Duration(milliseconds: 500), - message: lastModifiedStr, - child: Text( - lastModifiedStr, - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )), - )), - GestureDetector( - child: Tooltip( - waitDuration: Duration(milliseconds: 500), - message: sizeStr, - child: Text( - sizeStr, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 10, - color: MyTheme.darkGray), - ))), - ]), - ), + return Padding( + padding: EdgeInsets.symmetric(vertical: 1), + child: Container( + decoration: BoxDecoration( + color: isSelected + ? Theme.of(context).hoverColor + : Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(5.0), ), ), - ], - ), + key: ValueKey(entry.name), + height: kDesktopFileTransferRowHeight, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: InkWell( + child: Row( + children: [ + GestureDetector( + child: Container( + width: kDesktopFileTransferNameColWidth, + child: Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: entry.name, + child: Row(children: [ + entry.isDrive + ? Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)) + .paddingAll(4) + : SvgPicture.asset( + entry.isFile + ? "assets/file.svg" + : "assets/folder.svg", + color: Theme.of(context) + .tabBarTheme + .labelColor, + ), + Expanded( + child: Text( + entry.name.nonBreaking, + overflow: + TextOverflow.ellipsis)) + ]), + )), + onTap: () { + final items = getSelectedItems(isLocal); + // handle double click + if (_checkDoubleClick(entry)) { + openDirectory(entry.path, + isLocal: isLocal); + items.clear(); + return; + } + _onSelectedChanged( + items, filteredEntries, entry, isLocal); + }, + ), + Expanded( + child: GestureDetector( + child: SizedBox( + width: + kDesktopFileTransferModifiedColWidth, + child: Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + )), + ), + ), + ), + SizedBox( + width: 100, + child: GestureDetector( + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: sizeStr, + child: Text( + sizeStr, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10, + color: MyTheme.darkGray), + ), + ), + ), + ), + ], + ), + ), + ), + ], + )), ); }).toList(growable: false); @@ -520,98 +550,147 @@ class _FileManagerPageState extends State Widget statusList() { return PreferredSize( preferredSize: const Size(200, double.infinity), - child: Container( - margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration(border: Border.all(color: Colors.grey)), - child: Obx( - () => ListView.builder( - controller: ScrollController(), - itemBuilder: (BuildContext context, int index) { - final item = model.jobTable[index]; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Transform.rotate( - angle: item.isRemote ? pi : 0, - child: const Icon(Icons.send)), - const SizedBox( - width: 16.0, - ), - Expanded( + child: model.jobTable.isEmpty + ? Center(child: Text(translate("Empty"))) + : Container( + margin: + const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), + padding: const EdgeInsets.all(8.0), + child: Obx( + () => ListView.builder( + controller: ScrollController(), + itemBuilder: (BuildContext context, int index) { + final item = model.jobTable[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Tooltip( - waitDuration: Duration(milliseconds: 500), - message: item.jobName, - child: Text( - item.jobName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - )), - Wrap( + Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - '${item.display()} ${max(0, item.fileNum)}/${item.fileCount} '), - Text( - '${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), - Offstage( - offstage: - item.state != JobState.inProgress, - child: Text( - '${"${readableFileSize(item.speed)}/s"} ')), - Offstage( - offstage: item.totalSize <= 0, - child: Text( - '${(item.finishedSize.toDouble() * 100 / item.totalSize.toDouble()).toStringAsFixed(2)}%'), + Transform.rotate( + angle: item.isRemote ? pi : 0, + child: SvgPicture.asset( + "assets/arrow.svg", + color: Theme.of(context) + .tabBarTheme + .labelColor, + ), + ), + const SizedBox( + width: 16.0, + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: item.jobName, + child: Text( + item.jobName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Wrap( + children: [ + Text( + '${item.display()} ${max(0, item.fileNum)}/${item.fileCount} '), + Text( + '${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), + Offstage( + offstage: item.state != + JobState.inProgress, + child: Text( + '${"${readableFileSize(item.speed)}/s"} '), + ), + Offstage( + offstage: item.state != + JobState.inProgress, + child: LinearPercentIndicator( + padding: EdgeInsets.all(0), + width: MediaQuery.of(context) + .size + .width * + 0.15, + animateFromLastPercent: true, + center: Text( + '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', + ), + barRadius: Radius.circular(15), + percent: item.finishedSize / + item.totalSize, + progressColor: MyTheme.accent, + backgroundColor: + Color(0xFF4C4F62), + lineHeight: + kDesktopFileTransferRowHeight, + ).paddingSymmetric(vertical: 15), + ), + ], + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Offstage( + offstage: item.state != JobState.paused, + child: MenuButton( + onPressed: () { + model.resumeJob(item.id); + }, + child: SvgPicture.asset( + "assets/refresh.svg", + color: Theme.of(context) + .tabBarTheme + .labelColor, + ), + color: MyTheme.accent, + hoverColor: MyTheme.accent80, + ), + ), + MenuButton( + padding: EdgeInsets.only(right: 15), + child: SvgPicture.asset( + "assets/close.svg", + color: Theme.of(context) + .tabBarTheme + .labelColor, + ), + onPressed: () { + model.jobTable.removeAt(index); + model.cancelJob(item.id); + }, + color: MyTheme.accent, + hoverColor: MyTheme.accent80, + ), + ], ), ], ), ], - ), + ).paddingSymmetric(vertical: 10), ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Offstage( - offstage: item.state != JobState.paused, - child: IconButton( - onPressed: () { - model.resumeJob(item.id); - }, - splashRadius: kDesktopIconButtonSplashRadius, - icon: const Icon(Icons.restart_alt_rounded)), - ), - IconButton( - icon: const Icon(Icons.close), - splashRadius: 1, - onPressed: () { - model.jobTable.removeAt(index); - model.cancelJob(item.id); - }, - ), - ], - ) - ], - ), - SizedBox( - height: 8.0, - ), - Divider( - height: 2.0, - ) - ], - ); - }, - itemCount: model.jobTable.length, - ), - ), - )); + ); + }, + itemCount: model.jobTable.length, + ), + ), + )); } Widget headTools(bool isLocal) { @@ -620,95 +699,128 @@ class _FileManagerPageState extends State final locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; final selectedItems = getSelectedItems(isLocal); return Container( - child: Column( - children: [ - // symbols - PreferredSize( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 50, - height: 50, - decoration: BoxDecoration(color: Colors.blue), - padding: EdgeInsets.all(8.0), - child: FutureBuilder( - future: bind.sessionGetPlatform( - id: _ffi.id, isRemote: !isLocal), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!.isNotEmpty) { - return getPlatformImage('${snapshot.data}'); - } else { - return CircularProgressIndicator( - color: Colors.white, - ); - } - })), - Text(isLocal - ? translate("Local Computer") - : translate("Remote Computer")) - .marginOnly(left: 8.0) - ], - ), - preferredSize: Size(double.infinity, 70)), - // buttons - Row( - children: [ - Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back), - splashRadius: kDesktopIconButtonSplashRadius, - onPressed: () { - selectedItems.clear(); - model.goBack(isLocal: isLocal); - }, - ), - IconButton( - icon: const Icon(Icons.arrow_upward), - splashRadius: kDesktopIconButtonSplashRadius, - onPressed: () { - selectedItems.clear(); - model.goToParentDirectory(isLocal: isLocal); - }, - ), - ], - ), - Expanded( - child: GestureDetector( - onTap: () { - locationStatus.value = - locationStatus.value == LocationStatus.bread - ? LocationStatus.pathLocation - : LocationStatus.bread; - Future.delayed(Duration.zero, () { - if (locationStatus.value == LocationStatus.pathLocation) { - locationFocus.requestFocus(); - } - }); - }, - child: Obx(() => Container( - decoration: BoxDecoration( - border: Border.all( - color: locationStatus.value == LocationStatus.bread - ? Colors.black12 - : Theme.of(context) - .colorScheme - .primary - .withOpacity(0.5))), + child: Column( + children: [ + // symbols + PreferredSize( child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - child: locationStatus.value == LocationStatus.bread - ? buildBread(isLocal) - : buildPathLocation(isLocal)), + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8)), + color: MyTheme.accent, + ), + padding: EdgeInsets.all(8.0), + child: FutureBuilder( + future: bind.sessionGetPlatform( + id: _ffi.id, isRemote: !isLocal), + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.data!.isNotEmpty) { + return getPlatformImage('${snapshot.data}'); + } else { + return CircularProgressIndicator( + color: Theme.of(context) + .tabBarTheme + .labelColor, + ); + } + })), + Text(isLocal + ? translate("Local Computer") + : translate("Remote Computer")) + .marginOnly(left: 8.0) ], - ))), - )), - Obx(() { - switch (locationStatus.value) { - case LocationStatus.bread: - return IconButton( + ), + preferredSize: Size(double.infinity, 70)) + .paddingOnly(bottom: 15), + // buttons + Row( + children: [ + Row( + children: [ + MenuButton( + padding: EdgeInsets.only( + right: 3, + ), + child: SvgPicture.asset( + "assets/arrow.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + onPressed: () { + selectedItems.clear(); + model.goBack(isLocal: isLocal); + }, + ), + MenuButton( + child: RotatedBox( + quarterTurns: 3, + child: SvgPicture.asset( + "assets/arrow.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + onPressed: () { + selectedItems.clear(); + model.goToParentDirectory(isLocal: isLocal); + }, + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 3.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 2.5), + child: GestureDetector( + onTap: () { + locationStatus.value = + locationStatus.value == LocationStatus.bread + ? LocationStatus.pathLocation + : LocationStatus.bread; + Future.delayed(Duration.zero, () { + if (locationStatus.value == + LocationStatus.pathLocation) { + locationFocus.requestFocus(); + } + }); + }, + child: Obx( + () => Container( + child: Row( + children: [ + Expanded( + child: locationStatus.value == + LocationStatus.bread + ? buildBread(isLocal) + : buildPathLocation(isLocal)), + ], + ), + ), + ), + ), + ), + ), + ), + ), + Obx(() { + switch (locationStatus.value) { + case LocationStatus.bread: + return MenuButton( onPressed: () { locationStatus.value = LocationStatus.fileSearchBar; final focusNode = @@ -716,49 +828,77 @@ class _FileManagerPageState extends State Future.delayed( Duration.zero, () => focusNode.requestFocus()); }, - splashRadius: kDesktopIconButtonSplashRadius, - icon: Icon(Icons.search)); - case LocationStatus.pathLocation: - return IconButton( - color: Theme.of(context).disabledColor, + child: SvgPicture.asset( + "assets/search.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ); + case LocationStatus.pathLocation: + return MenuButton( onPressed: null, - splashRadius: kDesktopIconButtonSplashRadius, - icon: Icon(Icons.close)); - case LocationStatus.fileSearchBar: - return IconButton( + child: SvgPicture.asset( + "assets/close.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), color: Theme.of(context).disabledColor, + hoverColor: Theme.of(context).hoverColor, + ); + case LocationStatus.fileSearchBar: + return MenuButton( onPressed: () { onSearchText("", isLocal); locationStatus.value = LocationStatus.bread; }, - splashRadius: 1, - icon: Icon(Icons.close)); - } - }), - IconButton( + child: SvgPicture.asset( + "assets/close.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ); + } + }), + MenuButton( + padding: EdgeInsets.only( + left: 3, + ), onPressed: () { model.refresh(isLocal: isLocal); }, - splashRadius: kDesktopIconButtonSplashRadius, - icon: const Icon(Icons.refresh)), - ], - ), - Row( - textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, - children: [ - Expanded( - child: Row( - mainAxisAlignment: - isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, - children: [ - IconButton( - onPressed: () { - model.goHome(isLocal: isLocal); - }, - icon: const Icon(Icons.home_outlined), - splashRadius: kDesktopIconButtonSplashRadius, - ), - IconButton( + child: SvgPicture.asset( + "assets/refresh.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + ], + ), + Row( + textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, + children: [ + Expanded( + child: Row( + mainAxisAlignment: + isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + MenuButton( + padding: EdgeInsets.only( + right: 3, + ), + onPressed: () { + model.goHome(isLocal: isLocal); + }, + child: SvgPicture.asset( + "assets/home.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + MenuButton( onPressed: () { final name = TextEditingController(); _ffi.dialogManager.show((setState, close) { @@ -800,9 +940,14 @@ class _FileManagerPageState extends State ); }); }, - splashRadius: kDesktopIconButtonSplashRadius, - icon: const Icon(Icons.create_new_folder_outlined)), - IconButton( + child: SvgPicture.asset( + "assets/folder_new.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + MenuButton( onPressed: validItems(selectedItems) ? () async { await (model.removeAction(selectedItems, @@ -810,32 +955,80 @@ class _FileManagerPageState extends State selectedItems.clear(); } : null, - splashRadius: kDesktopIconButtonSplashRadius, - icon: const Icon(Icons.delete_forever_outlined)), - menu(isLocal: isLocal), - ], + child: SvgPicture.asset( + "assets/trash.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + menu(isLocal: isLocal), + ], + ), ), - ), - TextButton.icon( + ElevatedButton.icon( + style: ButtonStyle( + padding: MaterialStateProperty.all(isLocal + ? EdgeInsets.only(left: 10) + : EdgeInsets.only(right: 10)), + backgroundColor: MaterialStateProperty.all( + selectedItems.length == 0 + ? MyTheme.accent80 + : MyTheme.accent, + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), + ), onPressed: validItems(selectedItems) ? () { model.sendFiles(selectedItems, isRemote: !isLocal); selectedItems.clear(); } : null, - icon: Transform.rotate( - angle: isLocal ? 0 : pi, - child: const Icon( - Icons.send, - ), - ), - label: Text( - isLocal ? translate('Send') : translate('Receive'), - )), - ], - ).marginOnly(top: 8.0) - ], - )); + icon: isLocal + ? Text( + translate('Send'), + textAlign: TextAlign.right, + style: TextStyle( + color: selectedItems.length == 0 + ? MyTheme.darkGray + : Colors.white, + ), + ) + : RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + "assets/arrow.svg", + color: selectedItems.length == 0 + ? MyTheme.darkGray + : Colors.white, + alignment: Alignment.bottomRight, + ), + ), + label: isLocal + ? SvgPicture.asset( + "assets/arrow.svg", + color: selectedItems.length == 0 + ? MyTheme.darkGray + : Colors.white, + ) + : Text( + translate('Receive'), + style: TextStyle( + color: selectedItems.length == 0 + ? MyTheme.darkGray + : Colors.white, + ), + ), + ), + ], + ).marginOnly(top: 8.0) + ], + ), + ); } bool validItems(SelectedItems items) { @@ -890,25 +1083,27 @@ class _FileManagerPageState extends State mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: Listener( - // handle mouse wheel - onPointerSignal: (e) { - if (e is PointerScrollEvent) { - final sc = getBreadCrumbScrollController(isLocal); - final scale = Platform.isWindows ? 2 : 4; - sc.jumpTo(sc.offset + e.scrollDelta.dy / scale); - } - }, - child: BreadCrumb( - items: items, - divider: Icon(Icons.chevron_right), - overflow: ScrollableOverflow( - controller: - getBreadCrumbScrollController(isLocal)), - ))), + child: Listener( + // handle mouse wheel + onPointerSignal: (e) { + if (e is PointerScrollEvent) { + final sc = getBreadCrumbScrollController(isLocal); + final scale = Platform.isWindows ? 2 : 4; + sc.jumpTo(sc.offset + e.scrollDelta.dy / scale); + } + }, + child: BreadCrumb( + items: items, + divider: const Icon(Icons.keyboard_arrow_right_rounded), + overflow: ScrollableOverflow( + controller: getBreadCrumbScrollController(isLocal), + ), + ), + ), + ), ActionIcon( message: "", - icon: Icons.arrow_drop_down, + icon: Icons.keyboard_arrow_down_rounded, onTap: () async { final renderBox = locationBarKey.currentContext ?.findRenderObject() as RenderBox; @@ -1021,13 +1216,23 @@ class _FileManagerPageState extends State .marginSymmetric(horizontal: 4))); } else { final list = PathUtil.split(path, isWindows); - breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem( - content: TextButton( + breadCrumbList.addAll( + list.asMap().entries.map( + (e) => BreadCrumbItem( + content: TextButton( child: Text(e.value), style: ButtonStyle( - minimumSize: MaterialStateProperty.all(Size(0, 0))), - onPressed: () => onPressed(list.sublist(0, e.key + 1))) - .marginSymmetric(horizontal: 4)))); + minimumSize: MaterialStateProperty.all( + Size(0, 0), + ), + ), + onPressed: () => onPressed( + list.sublist(0, e.key + 1), + ), + ).marginSymmetric(horizontal: 4), + ), + ), + ); } return breadCrumbList; } @@ -1054,29 +1259,35 @@ class _FileManagerPageState extends State : searchTextObs.value; final textController = TextEditingController(text: text) ..selection = TextSelection.collapsed(offset: text.length); - return Row(children: [ - Icon( - locationStatus.value == LocationStatus.pathLocation - ? Icons.folder - : Icons.search, - color: Theme.of(context).hintColor, - ).paddingSymmetric(horizontal: 2), - Expanded( + return Row( + children: [ + SvgPicture.asset( + locationStatus.value == LocationStatus.pathLocation + ? "assets/folder.svg" + : "assets/search.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + Expanded( child: TextField( - focusNode: focusNode, - decoration: InputDecoration( - border: InputBorder.none, - isDense: true, - prefix: Padding(padding: EdgeInsets.only(left: 4.0))), - controller: textController, - onSubmitted: (path) { - openDirectory(path, isLocal: isLocal); - }, - onChanged: locationStatus.value == LocationStatus.fileSearchBar - ? (searchText) => onSearchText(searchText, isLocal) - : null, - )) - ]); + focusNode: focusNode, + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + prefix: Padding( + padding: EdgeInsets.only(left: 4.0), + ), + ), + controller: textController, + onSubmitted: (path) { + openDirectory(path, isLocal: isLocal); + }, + onChanged: locationStatus.value == LocationStatus.fileSearchBar + ? (searchText) => onSearchText(searchText, isLocal) + : null, + ), + ) + ], + ); } onSearchText(String searchText, bool isLocal) { @@ -1145,12 +1356,13 @@ class _FileManagerPageState extends State Text( name, style: headerTextStyle, - ).marginSymmetric( - horizontal: sortBy == SortBy.name ? 4 : 0.0), + ).marginSymmetric(horizontal: 4), ascending.value != null - ? Icon(ascending.value! - ? Icons.arrow_upward - : Icons.arrow_downward) + ? Icon( + ascending.value! + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + ) : const Offstage() ], ), diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 7540f7662..bbe2b28be 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -86,18 +86,14 @@ class _FileManagerTabPageState extends State { @override Widget build(BuildContext context) { - final tabWidget = Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: DesktopTab( - controller: tabController, - onWindowCloseButton: handleWindowCloseButton, - tail: const AddButton().paddingOnly(left: 10), - labelGetter: DesktopTab.labelGetterAlias, - )), - ); + final tabWidget = Scaffold( + backgroundColor: Theme.of(context).cardColor, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton().paddingOnly(left: 10), + labelGetter: DesktopTab.labelGetterAlias, + )); return Platform.isMacOS ? tabWidget : SubWindowDragToResizeArea( diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index 96cc9fa9b..df2c48ab4 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -27,6 +27,7 @@ class MenuButton extends StatefulWidget { class _MenuButtonState extends State { bool _isHover = false; + final double _borderRadius = 8.0; @override Widget build(BuildContext context) { @@ -38,16 +39,17 @@ class _MenuButtonState extends State { type: MaterialType.transparency, child: Ink( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), + borderRadius: BorderRadius.circular(_borderRadius), color: _isHover ? widget.hoverColor : widget.color, ), child: InkWell( + hoverColor: widget.hoverColor, onHover: (val) { setState(() { _isHover = val; }); }, - borderRadius: BorderRadius.circular(5), + borderRadius: BorderRadius.circular(_borderRadius), splashColor: widget.splashColor, enableFeedback: widget.enableFeedback, onTap: widget.onPressed, diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 91a061fb9..64c44a555 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -970,6 +970,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.1" + percent_indicator: + dependency: "direct main" + description: + name: percent_indicator + sha256: cec41f67181fbd5322aa68b355621d1a4eea827426b8eeb613f6cbe195ff7b4a + url: "https://pub.dev" + source: hosted + version: "4.2.2" petitparser: dependency: transitive description: @@ -1547,5 +1555,5 @@ packages: source: hosted version: "0.1.1" sdks: - dart: ">=2.18.0 <4.0.0" + dart: ">=2.18.0 <3.0.0" flutter: ">=3.3.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index df29252c9..7789f92f4 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -92,6 +92,7 @@ dependencies: password_strength: ^0.2.0 flutter_launcher_icons: ^0.11.0 flutter_keyboard_visibility: ^5.4.0 + percent_indicator: ^4.2.2 dev_dependencies: From b5ca85fb9b8566f80ce8f2d76c387b10634becdc Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 22 Feb 2023 22:44:06 +0100 Subject: [PATCH 156/202] fix colors in light theme --- .../lib/desktop/pages/file_manager_page.dart | 93 ++++++++++--------- 1 file changed, 51 insertions(+), 42 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 262121f3d..d42a28292 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -567,7 +567,7 @@ class _FileManagerPageState extends State decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.all( - Radius.circular(8.0), + Radius.circular(15.0), ), ), child: Column( @@ -584,7 +584,7 @@ class _FileManagerPageState extends State .tabBarTheme .labelColor, ), - ), + ).paddingOnly(left: 15), const SizedBox( width: 16.0, ), @@ -602,44 +602,57 @@ class _FileManagerPageState extends State item.jobName, maxLines: 1, overflow: TextOverflow.ellipsis, + ).paddingSymmetric(vertical: 10), + ), + Text( + '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}', + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, ), ), - Wrap( - children: [ - Text( - '${item.display()} ${max(0, item.fileNum)}/${item.fileCount} '), - Text( - '${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), - Offstage( - offstage: item.state != - JobState.inProgress, - child: Text( - '${"${readableFileSize(item.speed)}/s"} '), + Offstage( + offstage: + item.state != JobState.inProgress, + child: Text( + '${translate("Speed")} ${readableFileSize(item.speed)}/s', + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, ), - Offstage( - offstage: item.state != - JobState.inProgress, - child: LinearPercentIndicator( - padding: EdgeInsets.all(0), - width: MediaQuery.of(context) - .size - .width * - 0.15, - animateFromLastPercent: true, - center: Text( - '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', - ), - barRadius: Radius.circular(15), - percent: item.finishedSize / - item.totalSize, - progressColor: MyTheme.accent, - backgroundColor: - Color(0xFF4C4F62), - lineHeight: - kDesktopFileTransferRowHeight, - ).paddingSymmetric(vertical: 15), + ), + ), + Offstage( + offstage: + item.state == JobState.inProgress, + child: Text( + translate( + item.display(), ), - ], + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + ), + ), + Offstage( + offstage: + item.state != JobState.inProgress, + child: LinearPercentIndicator( + padding: EdgeInsets.only(right: 15), + animateFromLastPercent: true, + center: Text( + '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', + ), + barRadius: Radius.circular(15), + percent: item.finishedSize / + item.totalSize, + progressColor: MyTheme.accent, + backgroundColor: + Theme.of(context).hoverColor, + lineHeight: + kDesktopFileTransferRowHeight, + ).paddingSymmetric(vertical: 15), ), ], ), @@ -655,9 +668,7 @@ class _FileManagerPageState extends State }, child: SvgPicture.asset( "assets/refresh.svg", - color: Theme.of(context) - .tabBarTheme - .labelColor, + color: Colors.white, ), color: MyTheme.accent, hoverColor: MyTheme.accent80, @@ -667,9 +678,7 @@ class _FileManagerPageState extends State padding: EdgeInsets.only(right: 15), child: SvgPicture.asset( "assets/close.svg", - color: Theme.of(context) - .tabBarTheme - .labelColor, + color: Colors.white, ), onPressed: () { model.jobTable.removeAt(index); From 85a82a6ba74d1fe2d276a7bf756783d275f229c7 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 22 Feb 2023 22:47:09 +0100 Subject: [PATCH 157/202] added svgs --- flutter/assets/arrow.svg | 2 ++ flutter/assets/dots.svg | 2 ++ flutter/assets/file.svg | 2 ++ flutter/assets/folder.svg | 2 ++ flutter/assets/folder_new.svg | 2 ++ flutter/assets/home.svg | 2 ++ flutter/assets/refresh.svg | 2 ++ flutter/assets/search.svg | 2 ++ flutter/assets/trash.svg | 2 ++ 9 files changed, 18 insertions(+) create mode 100644 flutter/assets/arrow.svg create mode 100644 flutter/assets/dots.svg create mode 100644 flutter/assets/file.svg create mode 100644 flutter/assets/folder.svg create mode 100644 flutter/assets/folder_new.svg create mode 100644 flutter/assets/home.svg create mode 100644 flutter/assets/refresh.svg create mode 100644 flutter/assets/search.svg create mode 100644 flutter/assets/trash.svg diff --git a/flutter/assets/arrow.svg b/flutter/assets/arrow.svg new file mode 100644 index 000000000..d0f032bc2 --- /dev/null +++ b/flutter/assets/arrow.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/dots.svg b/flutter/assets/dots.svg new file mode 100644 index 000000000..19563b849 --- /dev/null +++ b/flutter/assets/dots.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/file.svg b/flutter/assets/file.svg new file mode 100644 index 000000000..21c7fb9de --- /dev/null +++ b/flutter/assets/file.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/folder.svg b/flutter/assets/folder.svg new file mode 100644 index 000000000..3959f7874 --- /dev/null +++ b/flutter/assets/folder.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/folder_new.svg b/flutter/assets/folder_new.svg new file mode 100644 index 000000000..22b729204 --- /dev/null +++ b/flutter/assets/folder_new.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/home.svg b/flutter/assets/home.svg new file mode 100644 index 000000000..45a018f5d --- /dev/null +++ b/flutter/assets/home.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/refresh.svg b/flutter/assets/refresh.svg new file mode 100644 index 000000000..f77fcfd4c --- /dev/null +++ b/flutter/assets/refresh.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/search.svg b/flutter/assets/search.svg new file mode 100644 index 000000000..295136d7e --- /dev/null +++ b/flutter/assets/search.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/trash.svg b/flutter/assets/trash.svg new file mode 100644 index 000000000..f9037e0e1 --- /dev/null +++ b/flutter/assets/trash.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file From 922a70adb45ae15a16376dd096a0fbda48c4fdb8 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 22 Feb 2023 22:52:29 +0100 Subject: [PATCH 158/202] removed filesize expanded --- .../lib/desktop/pages/file_manager_page.dart | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index d42a28292..0d55552af 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -406,23 +406,20 @@ class _FileManagerPageState extends State items, filteredEntries, entry, isLocal); }, ), - Expanded( - child: GestureDetector( - child: SizedBox( - width: - kDesktopFileTransferModifiedColWidth, - child: Tooltip( - waitDuration: - Duration(milliseconds: 500), - message: lastModifiedStr, - child: Text( - lastModifiedStr, - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - )), - ), + GestureDetector( + child: SizedBox( + width: kDesktopFileTransferModifiedColWidth, + child: Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + )), ), ), SizedBox( From 12a33cdfbb6b3161afce023d0675c3928dcded7c Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 22 Feb 2023 23:01:31 +0100 Subject: [PATCH 159/202] Merge remote-tracking branch 'upstream/master' into file-manager-redesign --- .devcontainer/Dockerfile | 53 +++- .devcontainer/build.sh | 75 +++++ .devcontainer/devcontainer.json | 21 +- .devcontainer/setup.sh | 23 ++ .github/ISSUE_TEMPLATE/bug_report.yaml | 4 +- .github/workflows/ci.yml | 5 + .github/workflows/flutter-ci.yml | 5 + Cargo.lock | 2 +- Cargo.toml | 1 - README.md | 10 +- docs/DEVCONTAINER.md | 14 + flutter/build_android.sh | 10 +- flutter/lib/common/widgets/address_book.dart | 17 +- flutter/lib/common/widgets/remote_input.dart | 11 +- flutter/lib/consts.dart | 1 + .../lib/desktop/pages/desktop_home_page.dart | 5 + .../lib/desktop/pages/remote_tab_page.dart | 2 + .../lib/desktop/widgets/remote_menubar.dart | 6 + .../lib/desktop/widgets/tabbar_widget.dart | 15 +- flutter/lib/models/input_model.dart | 4 + flutter/pubspec.lock | 12 +- flutter/pubspec.yaml | 265 +++++++++--------- libs/hbb_common/Cargo.toml | 1 + libs/hbb_common/src/lib.rs | 1 + libs/scrap/src/common/hwcodec.rs | 38 ++- res/rustdesk.desktop | 4 +- src/flutter_ffi.rs | 9 +- src/ipc.rs | 2 +- src/lang/cn.rs | 146 +++++----- src/lang/fa.rs | 6 +- src/platform/macos.rs | 2 +- 31 files changed, 488 insertions(+), 282 deletions(-) create mode 100755 .devcontainer/build.sh create mode 100755 .devcontainer/setup.sh create mode 100644 docs/DEVCONTAINER.md diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 0381ff966..32a440b28 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,19 +1,50 @@ -FROM debian +FROM mcr.microsoft.com/devcontainers/base:ubuntu-22.04 +ENV HOME=/home/vscode +ENV WORKDIR=$HOME/rustdesk + +WORKDIR $HOME +RUN sudo apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +WORKDIR / + +RUN git clone https://github.com/microsoft/vcpkg +WORKDIR vcpkg +RUN git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82 +RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics +ENV VCPKG_ROOT=/vcpkg +RUN $VCPKG_ROOT/vcpkg --disable-metrics install libvpx libyuv opus WORKDIR / -RUN apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +RUN wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz && tar xzf dep.tar.gz -RUN git clone https://github.com/microsoft/vcpkg && cd vcpkg && git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82 -RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics -RUN /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus -RUN groupadd -r user && useradd -r -g user user --home /home/user && mkdir -p /home/user && chown user /home/user && echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user -WORKDIR /home/user +USER vscode +WORKDIR $HOME RUN wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so -USER user RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh RUN chmod +x rustup.sh -RUN ./rustup.sh -y +RUN $HOME/rustup.sh -y +RUN $HOME/.cargo/bin/rustup target add aarch64-linux-android +RUN $HOME/.cargo/bin/cargo install cargo-ndk -USER root -ENV HOME=/home/user +# Install Flutter +RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.3-stable.tar.xz +RUN tar xf flutter_linux_3.7.3-stable.tar.xz && rm flutter_linux_3.7.3-stable.tar.xz +ENV PATH="$PATH:$HOME/flutter/bin" +RUN dart pub global activate ffigen 5.0.1 + + +# Install packages +RUN sudo apt-get install -y libclang-dev +RUN sudo apt install -y gcc-multilib + +WORKDIR $WORKDIR +ENV ANDROID_NDK_HOME=/opt/android/ndk/22.1.7171670 + +# Somehow try to automate flutter pub get +# https://rustdesk.com/docs/en/dev/build/android/ +# Put below steps in entrypoint.sh +# cd flutter +# wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz +# tar xzf so.tar.gz + +# own /opt/android diff --git a/.devcontainer/build.sh b/.devcontainer/build.sh new file mode 100755 index 000000000..df87aace7 --- /dev/null +++ b/.devcontainer/build.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +set -e + +MODE=${1:---debug} +TYPE=${2:-linux} +MODE=${MODE/*-/} + + +build(){ + pwd + $WORKDIR/entrypoint $1 +} + +build_arm64(){ + CWD=$(pwd) + cd $WORKDIR/flutter + flutter pub get + cd $WORKDIR + $WORKDIR/flutter/ndk_arm64.sh + cp $WORKDIR/target/aarch64-linux-android/release/liblibrustdesk.so $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + cd $CWD +} + +build_apk(){ + cd $WORKDIR/flutter + MODE=$1 $WORKDIR/flutter/build_android.sh + cd $WORKDIR +} + +key_gen(){ + if [ ! -f $WORKDIR/flutter/android/key.properties ] + then + if [ ! -f $HOME/upload-keystore.jks ] + then + $WORKDIR/.devcontainer/setup.sh key + fi + read -r -p "enter the password used to generate $HOME/upload-keystore.jks\n" password + echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties + else + echo "Believing storeFile is created ref: $WORKDIR/flutter/android/key.properties" + fi +} + +android_build(){ + if [ ! -d $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a ] + then + $WORKDIR/.devcontainer/setup.sh android + fi + build_arm64 + case $1 in + debug) + build_apk debug + ;; + release) + key_gen + build_apk release + ;; + esac +} + +case "$MODE:$TYPE" in + "debug:linux") + build + ;; + "release:linux") + build --release + ;; + "debug:android") + android_build debug + ;; + "release:android") + android_build release + ;; +esac diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 24ba9a915..cd82c75e3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,15 +1,18 @@ { "name": "rustdesk", "build": { - "dockerfile": "Dockerfile", - "args": { - "BUILDKIT_INLINE_CACHE": "0" + "dockerfile": "./Dockerfile", + "context": "." + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache", + "workspaceFolder": "/home/vscode/rustdesk", + "postStartCommand": ".devcontainer/build.sh", + "features": { + "ghcr.io/devcontainers/features/java:1": {}, + "ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": { + "PACKAGES": "platform-tools,ndk;22.1.7171670" } }, - "workspaceMount": "source=${localWorkspaceFolder},target=/home/user/rustdesk,type=bind,consistency=cache", - "workspaceFolder": "/home/user/rustdesk", - "postStartCommand": "./entrypoint", - "remoteUser": "user", "customizations": { "vscode": { "extensions": [ @@ -17,7 +20,9 @@ "mutantdino.resourcemonitor", "rust-lang.rust-analyzer", "tamasfe.even-better-toml", - "serayuzgur.crates" + "serayuzgur.crates", + "mhutchie.git-graph", + "eamodio.gitlens" ], "settings": { "files.watcherExclude": { diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh new file mode 100755 index 000000000..c972f47b2 --- /dev/null +++ b/.devcontainer/setup.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e +case $1 in + android) + # install deps + cd $WORKDIR/flutter + flutter pub get + wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz + tar xzf so.tar.gz + rm so.tar.gz + sudo chown -R $(whoami) $ANDROID_HOME + echo "Setup is Done." + ;; + linux) + echo "Linux Setup" + ;; + key) + echo -e "\n$HOME/upload-keystore.jks is not created.\nLet's create it.\nRemember the password you enter in keytool!" + keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload + ;; +esac + + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index ec23aa7a9..fea1a3672 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -44,7 +44,9 @@ body: id: screenshots attributes: label: Screenshots - description: If applicable, please add screenshots to help explain your problem + description: Please add screenshots to help explain your problem, if applicable, please upload video. + validations: + required: true - type: textarea id: context attributes: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e1702a60..bba114315 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,9 @@ name: CI on: workflow_dispatch: pull_request: + paths-ignore: + - "docs/**" + - "README.md" push: branches: - master @@ -14,6 +17,8 @@ on: - '*' paths-ignore: - ".github/**" + - "docs/**" + - "README.md" jobs: # ensure_cargo_fmt: diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 78c60df37..2386f17dd 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -3,6 +3,9 @@ name: Full Flutter CI on: workflow_dispatch: pull_request: + paths-ignore: + - "docs/**" + - "README.md" push: branches: - master @@ -10,6 +13,8 @@ on: - '*' paths-ignore: - ".github/**" + - "docs/**" + - "README.md" env: LLVM_VERSION: "15.0.6" diff --git a/Cargo.lock b/Cargo.lock index 48981e169..115845b50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2623,6 +2623,7 @@ dependencies = [ "serde_json 1.0.89", "socket2 0.3.19", "sodiumoxide", + "sysinfo", "tokio", "tokio-socks", "tokio-util", @@ -4887,7 +4888,6 @@ dependencies = [ "shutdown_hooks", "simple_rc", "sys-locale", - "sysinfo", "system_shutdown", "tao", "tray-icon", diff --git a/Cargo.toml b/Cargo.toml index 0ebe49fdf..f685e3f2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,6 @@ uuid = { version = "1.0", features = ["v4"] } clap = "3.0" rpassword = "7.0" base64 = "0.13" -sysinfo = "0.24" num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } default-net = "0.12.0" diff --git a/README.md b/README.md index df0ca8328..8af79915b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Yet another remote desktop software, written in Rust. Works out of the box, no c RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for help getting started. -[**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) @@ -41,6 +41,14 @@ Below are the servers you are using for free, they may change over time. If you | USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | | Ukraine (Kyiv) | dc.volia (2VM) | 2 vCPU / 4GB RAM | +## Dev Container + +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk) + +If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. + +Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info. + ## Dependencies Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only. diff --git a/docs/DEVCONTAINER.md b/docs/DEVCONTAINER.md new file mode 100644 index 000000000..067e0ecf9 --- /dev/null +++ b/docs/DEVCONTAINER.md @@ -0,0 +1,14 @@ + +After the start of devcontainer in docker container, a linux binary in debug mode is created. + +Currently devcontainer offers linux and android builds in both debug and release mode. + +Below is the table on commands to run from root of the project for creating specific builds. + +Command|Build Type|Mode +-|-|-| +`.devcontainer/build.sh --debug linux`|Linux|debug +`.devcontainer/build.sh --release linux`|Linux|release +`.devcontainer/build.sh --debug android`|android-arm64|debug +`.devcontainer/build.sh --release android`|android-arm64|debug + diff --git a/flutter/build_android.sh b/flutter/build_android.sh index 01ff23488..c6b639f87 100755 --- a/flutter/build_android.sh +++ b/flutter/build_android.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash -$ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* -flutter build apk --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info -flutter build apk ---split-per-abi --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info -flutter build appbundle --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info + +MODE=${MODE:=release} +$ANDROID_NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* +flutter build apk --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info +flutter build apk --split-per-abi --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info +flutter build appbundle --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info # build in linux # $ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index bd2a01296..88a5aaaa3 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -43,11 +43,8 @@ class _AddressBookState extends State { return Obx(() { if (gFFI.userModel.userName.value.isEmpty) { return Center( - child: ElevatedButton( - onPressed: loginDialog, - child: Text(translate("Login")) - ) - ); + child: ElevatedButton( + onPressed: loginDialog, child: Text(translate("Login")))); } else { if (gFFI.abModel.abLoading.value) { return const Center( @@ -153,13 +150,13 @@ class _AddressBookState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(translate('Tags')), - GestureDetector( - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; + Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; menuPos = RelativeRect.fromLTRB(x, y, x, y); }, - onTap: () => _showMenu(menuPos), + onPointerUp: (_) => _showMenu(menuPos), child: ActionMore()), ], ); diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 5833e760d..dd39cbdfd 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_hbb/models/state_model.dart'; -import '../../common.dart'; import '../../models/input_model.dart'; class RawKeyFocusScope extends StatelessWidget { @@ -20,13 +18,6 @@ class RawKeyFocusScope extends StatelessWidget { @override Widget build(BuildContext context) { - final FocusOnKeyCallback? onKey; - if (isAndroid) { - onKey = inputModel.handleRawKeyEvent; - } else { - onKey = stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null; - } - return FocusScope( autofocus: true, child: Focus( @@ -34,7 +25,7 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKey: onKey, + onKey: inputModel.handleRawKeyEvent, child: child)); } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 22ba221a9..2b73182fd 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -20,6 +20,7 @@ const String kAppTypeDesktopPortForward = "port forward"; const String kWindowMainWindowOnTop = "main_window_on_top"; const String kWindowGetWindowInfo = "get_window_info"; +const String kWindowDisableGrabKeyboard = "disable_grab_keyboard"; const String kWindowActionRebuild = "rebuild"; const String kWindowEventHide = "hide"; const String kWindowEventShow = "show"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index b5cadbcdf..ff99c9dc8 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -14,6 +14,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -498,6 +499,10 @@ class _DesktopHomePageState extends State if (watchIsInputMonitoring) { if (bind.mainIsCanInputMonitoring(prompt: false)) { watchIsInputMonitoring = false; + // Do not notify for now. + // Monitoring may not take effect until the process is restarted. + // rustDeskWinManager.call( + // WindowType.RemoteDesktop, kWindowDisableGrabKeyboard, ''); setState(() {}); } } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 64c78f24d..ef3a0dd04 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -111,6 +111,8 @@ class _ConnectionTabPageState extends State { forceRelay: args['forceRelay'], ), )); + } else if (call.method == kWindowDisableGrabKeyboard) { + stateGlobal.grabKeyboard = false; } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index e82e9d26e..45857aa45 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -650,6 +650,12 @@ class _RemoteMenubarState extends State { } Widget _buildKeyboard(BuildContext context) { + // Do not support peer 1.1.9. + if (stateGlobal.grabKeyboard) { + bind.sessionSetKeyboardMode(id: widget.id, value: 'map'); + return Offstage(); + } + FfiModel ffiModel = Provider.of(context); if (ffiModel.permissions['keyboard'] == false) { return Offstage(); diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 9ba7a6315..ee3aaaf2c 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -548,13 +548,20 @@ class WindowActionPanelState extends State if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) { await rustDeskWinManager.unregisterActiveWindow(kMainWindowId); } - // `hide` must be placed after unregisterActiveWindow, because once all windows are hidden, - // flutter closes the application on macOS. We should ensure the post-run logic has ran successfully. - // e.g.: saving window position. + // macOS specific workaround, the window is not hiding when in fullscreen. + if (Platform.isMacOS && await windowManager.isFullScreen()) { + await windowManager.setFullScreen(false); + await Future.delayed(Duration(seconds: 1)); + } await windowManager.hide(); } else { // it's safe to hide the subwindow - await WindowController.fromWindowId(kWindowId!).hide(); + final controller = WindowController.fromWindowId(kWindowId!); + if (Platform.isMacOS && await controller.isFullScreen()) { + await controller.setFullscreen(false); + await Future.delayed(Duration(seconds: 1)); + } + await controller.hide(); await Future.wait([ rustDeskWinManager .call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}), diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index b1491d526..9a5b06b14 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -58,6 +58,10 @@ class InputModel { InputModel(this.parent); KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { + if (!stateGlobal.grabKeyboard) { + return KeyEventResult.handled; + } + // * Currently mobile does not enable map mode if (isDesktop) { bind.sessionGetKeyboardMode(id: id).then((result) { diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 64c44a555..a07df9c2e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -325,8 +325,8 @@ packages: dependency: "direct main" description: path: "." - ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 - resolved-ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 + ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a + resolved-ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -1224,6 +1224,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + texture_rgba_renderer: + dependency: "direct main" + description: + name: texture_rgba_renderer + sha256: fbb09b2c6b4ce71261927f9e7e4ea339af3e2f3f2b175f6fb921de1c66ec848d + url: "https://pub.dev" + source: hosted + version: "0.0.8" timing: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 7789f92f4..667b3645e 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -19,156 +19,153 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.2.0 environment: - sdk: ">=2.17.0" + sdk: ">=2.17.0" dependencies: - flutter: - sdk: flutter - flutter_localizations: - sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.3 - ffi: ^2.0.1 - path_provider: ^2.0.12 - external_path: ^1.0.1 - provider: ^6.0.3 - tuple: ^2.0.0 - wakelock: ^0.6.2 - device_info_plus: ^4.1.2 - #firebase_analytics: ^9.1.5 - package_info_plus: ^1.4.2 - url_launcher: ^6.0.9 - toggle_switch: ^1.4.0 - dash_chat_2: ^0.0.14 - draggable_float_widget: ^0.0.2 - settings_ui: ^2.0.2 - flutter_breadcrumb: ^1.0.1 - http: ^0.13.4 - qr_code_scanner: ^1.0.0 - zxing2: ^0.1.0 - image_picker: ^0.8.5 - image: ^3.1.3 - back_button_interceptor: ^6.0.1 - flutter_rust_bridge: ^1.61.1 - window_manager: - git: - url: https://github.com/Kingtous/rustdesk_window_manager - ref: 32b24c66151b72bba033ef8b954486aa9351d97b - desktop_multi_window: - git: - url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 - freezed_annotation: ^2.0.3 - flutter_custom_cursor: ^0.0.4 - window_size: - git: - url: https://github.com/google/flutter-desktop-embedding.git - path: plugins/window_size - ref: a738913c8ce2c9f47515382d40827e794a334274 - get: ^4.6.5 - visibility_detector: ^0.3.3 - contextmenu: ^3.0.0 - desktop_drop: ^0.3.3 - scroll_pos: ^0.3.0 - debounce_throttle: ^2.0.0 - file_picker: ^5.1.0 - flutter_svg: ^1.1.5 - flutter_improved_scrolling: - # currently, we use flutter 3.0.5 for windows build, latest for other builds. - # - # for flutter 3.0.5, please use official version(just comment code below). - # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). - git: - url: https://github.com/Kingtous/flutter_improved_scrolling - ref: 62f09545149f320616467c306c8c5f71714a18e6 - uni_links: ^0.5.1 - uni_links_desktop: ^0.1.4 - path: ^1.8.1 - auto_size_text: ^3.0.0 - bot_toast: ^4.0.3 - win32: any - password_strength: ^0.2.0 - flutter_launcher_icons: ^0.11.0 - flutter_keyboard_visibility: ^5.4.0 - percent_indicator: ^4.2.2 + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.3 + ffi: ^2.0.1 + path_provider: ^2.0.12 + external_path: ^1.0.1 + provider: ^6.0.3 + tuple: ^2.0.0 + wakelock: ^0.6.2 + device_info_plus: ^4.1.2 + #firebase_analytics: ^9.1.5 + package_info_plus: ^1.4.2 + url_launcher: ^6.0.9 + toggle_switch: ^1.4.0 + dash_chat_2: ^0.0.14 + draggable_float_widget: ^0.0.2 + settings_ui: ^2.0.2 + flutter_breadcrumb: ^1.0.1 + http: ^0.13.4 + qr_code_scanner: ^1.0.0 + zxing2: ^0.1.0 + image_picker: ^0.8.5 + image: ^3.1.3 + back_button_interceptor: ^6.0.1 + flutter_rust_bridge: ^1.61.1 + window_manager: + git: + url: https://github.com/Kingtous/rustdesk_window_manager + ref: 32b24c66151b72bba033ef8b954486aa9351d97b + desktop_multi_window: + git: + url: https://github.com/Kingtous/rustdesk_desktop_multi_window + ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a + freezed_annotation: ^2.0.3 + flutter_custom_cursor: ^0.0.4 + window_size: + git: + url: https://github.com/google/flutter-desktop-embedding.git + path: plugins/window_size + ref: a738913c8ce2c9f47515382d40827e794a334274 + get: ^4.6.5 + visibility_detector: ^0.3.3 + contextmenu: ^3.0.0 + desktop_drop: ^0.3.3 + scroll_pos: ^0.3.0 + debounce_throttle: ^2.0.0 + file_picker: ^5.1.0 + flutter_svg: ^1.1.5 + flutter_improved_scrolling: + # currently, we use flutter 3.0.5 for windows build, latest for other builds. + # + # for flutter 3.0.5, please use official version(just comment code below). + # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). + git: + url: https://github.com/Kingtous/flutter_improved_scrolling + ref: 62f09545149f320616467c306c8c5f71714a18e6 + uni_links: ^0.5.1 + uni_links_desktop: ^0.1.4 + path: ^1.8.1 + auto_size_text: ^3.0.0 + bot_toast: ^4.0.3 + win32: any + password_strength: ^0.2.0 + flutter_launcher_icons: ^0.11.0 + flutter_keyboard_visibility: ^5.4.0 + percent_indicator: ^4.2.2 + texture_rgba_renderer: ^0.0.8 dev_dependencies: - icons_launcher: ^2.0.4 - #flutter_test: - #sdk: flutter - build_runner: ^2.1.11 - freezed: ^2.0.3 - flutter_lints: ^2.0.0 - ffigen: ^7.2.4 + icons_launcher: ^2.0.4 + #flutter_test: + #sdk: flutter + build_runner: ^2.1.11 + freezed: ^2.0.3 + flutter_lints: ^2.0.0 + ffigen: ^7.2.4 # rerun: flutter pub run flutter_launcher_icons flutter_icons: - image_path: "../res/icon.png" - remove_alpha_ios: true - android: true - ios: true - windows: - generate: true - macos: - image_path: "../res/mac-icon.png" - generate: true - linux: true - web: - generate: true - + image_path: "../res/icon.png" + remove_alpha_ios: true + android: true + ios: true + windows: + generate: true + macos: + image_path: "../res/mac-icon.png" + generate: true + linux: true + web: + generate: true # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true - # To add assets to your application, add an assets section, like this: - assets: - - assets/ + # To add assets to your application, add an assets section, like this: + assets: + - assets/ - fonts: - - family: GestureIcons - fonts: - - asset: assets/gestures.ttf - - family: Tabbar - fonts: - - asset: assets/tabbar.ttf - - family: PeerSearchbar - fonts: - - asset: assets/peer_searchbar.ttf + fonts: + - family: GestureIcons + fonts: + - asset: assets/gestures.ttf + - family: Tabbar + fonts: + - asset: assets/tabbar.ttf + - family: PeerSearchbar + fonts: + - asset: assets/peer_searchbar.ttf - + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 0457bb19a..a125078d2 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -33,6 +33,7 @@ tokio-socks = { git = "https://github.com/open-trade/tokio-socks" } chrono = "0.4" backtrace = "0.3" libc = "0.2" +sysinfo = "0.24" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] mac_address = "1.1" diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 99cb6f408..bfb773908 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -42,6 +42,7 @@ pub use chrono; pub use libc; pub use directories_next; pub mod keyboard; +pub use sysinfo; #[cfg(feature = "quic")] pub type Stream = quic::Connection; diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 9cd6077a6..27b157b79 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -317,16 +317,30 @@ pub fn check_config() { } pub fn check_config_process(force_reset: bool) { - if force_reset { - HwCodecConfig::remove(); - } - if let Ok(exe) = std::env::current_exe() { - std::thread::spawn(move || { - std::process::Command::new(exe) - .arg("--check-hwcodec-config") - .status() - .ok(); - HwCodecConfig::refresh(); - }); - }; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; + + std::thread::spawn(move || { + if force_reset { + HwCodecConfig::remove(); + } + if let Ok(exe) = std::env::current_exe() { + if let Some(file_name) = exe.file_name().to_owned() { + let s = System::new_all(); + let arg = "--check-hwcodec-config"; + for process in s.processes_by_name(&file_name.to_string_lossy().to_string()) { + if process.cmd().iter().any(|cmd| cmd.contains(arg)) { + log::warn!("already have process {}", arg); + return; + } + } + if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() { + let second = 3; + std::thread::sleep(std::time::Duration::from_secs(second)); + // kill: Different platforms have different results + child.kill().ok(); + HwCodecConfig::refresh(); + } + } + }; + }); } diff --git a/res/rustdesk.desktop b/res/rustdesk.desktop index c9cf1f254..f31a16dec 100644 --- a/res/rustdesk.desktop +++ b/res/rustdesk.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=1.2.0 +Version=1.5 Name=RustDesk GenericName=Remote Desktop Comment=Remote Desktop @@ -16,4 +16,4 @@ X-Desktop-File-Install-Version=0.23 [Desktop Action new-window] Name=Open a New Window - +Exec=rustdesk %u diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f3bc45856..68ddce9b7 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,13 +1,13 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::get_default_sound_input; use crate::{ client::file_trait::FileManager, - common::make_fd_to_json, common::is_keyboard_mode_supported, + common::make_fd_to_json, flutter::{self, SESSIONS}, flutter::{session_add, session_start_}, ui_interface::{self, *}, }; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::get_default_sound_input; use flutter_rust_bridge::{StreamSink, SyncReturn}; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, @@ -1181,6 +1181,9 @@ pub fn main_start_grab_keyboard() -> SyncReturn { return SyncReturn(false); } crate::keyboard::client::start_grab_loop(); + if !is_can_input_monitoring(false) { + return SyncReturn(false); + } SyncReturn(true) } diff --git a/src/ipc.rs b/src/ipc.rs index 699b0bcd7..b1b130340 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -549,7 +549,7 @@ async fn check_pid(postfix: &str) { file.read_to_string(&mut content).ok(); let pid = content.parse::().unwrap_or(0); if pid > 0 { - use sysinfo::{ProcessExt, System, SystemExt}; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; let mut sys = System::new(); sys.refresh_processes(); if let Some(p) = sys.process(pid.into()) { diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 9d0d176da..4824ac5e9 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "状态"), ("Your Desktop", "你的桌面"), - ("desk_tip", "你的桌面可以通过下面的ID和密码访问。"), + ("desk_tip", "你的桌面可以通过下面的 ID 和密码访问。"), ("Password", "密码"), ("Ready", "就绪"), ("Established", "已建立"), @@ -11,7 +11,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Service", "允许服务"), ("Start Service", "启动服务"), ("Service is running", "服务正在运行"), - ("Service is not running", "服务没有启动"), + ("Service is not running", "服务未运行"), ("not_ready_status", "未就绪,请检查网络连接"), ("Control Remote Desktop", "控制远程桌面"), ("Transfer File", "传输文件"), @@ -19,49 +19,49 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recent Sessions", "最近访问过"), ("Address Book", "地址簿"), ("Confirmation", "确认"), - ("TCP Tunneling", "TCP隧道"), + ("TCP Tunneling", "TCP 隧道"), ("Remove", "删除"), ("Refresh random password", "刷新随机密码"), ("Set your own password", "设置密码"), ("Enable Keyboard/Mouse", "允许控制键盘/鼠标"), ("Enable Clipboard", "允许同步剪贴板"), ("Enable File Transfer", "允许传输文件"), - ("Enable TCP Tunneling", "允许建立TCP隧道"), - ("IP Whitelisting", "IP白名单"), + ("Enable TCP Tunneling", "允许建立 TCP 隧道"), + ("IP Whitelisting", "IP 白名单"), ("ID/Relay Server", "ID/中继服务器"), ("Import Server Config", "导入服务器配置"), ("Export Server Config", "导出服务器配置"), ("Import server configuration successfully", "导入服务器配置信息成功"), ("Export server configuration successfully", "导出服务器配置信息成功"), - ("Invalid server configuration", "无效服务器配置,请修改后重新拷贝配置信息到剪贴板后点击此按钮"), - ("Clipboard is empty", "拷贝配置信息到剪贴板后点击此按钮,可以自动导入配置"), + ("Invalid server configuration", "服务器配置无效,请修改后重新复制配置信息到剪贴板,然后点击此按钮"), + ("Clipboard is empty", "复制配置信息到剪贴板后点击此按钮,可以自动导入配置"), ("Stop service", "停止服务"), - ("Change ID", "改变ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), - ("id_change_tip", "只可以使用字母a-z, A-Z, 0-9, _ (下划线)。首字母必须是a-z, A-Z。长度在6与16之间。"), + ("Change ID", "更改 ID"), + ("Your new ID", "你的新 ID"), + ("length %min% to %max%", "长度在 %min 与 %max 之间"), + ("starts with a letter", "以字母开头"), + ("allowed characters", "使用允许的字符"), + ("id_change_tip", "只可以使用字母 a-z, A-Z, 0-9, _ (下划线)。首字母必须是 a-z, A-Z。长度在 6 与 16 之间。"), ("Website", "网站"), ("About", "关于"), ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Privacy Statement", "隐私声明"), ("Mute", "静音"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "构建日期"), + ("Version", "版本"), + ("Home", "主页"), ("Audio Input", "音频输入"), ("Enhancements", "增强功能"), ("Hardware Codec", "硬件编解码"), ("Adaptive Bitrate", "自适应码率"), - ("ID Server", "ID服务器"), + ("ID Server", "ID 服务器"), ("Relay Server", "中继服务器"), - ("API Server", "API服务器"), - ("invalid_http", "必须以http://或者https://开头"), - ("Invalid IP", "无效IP"), + ("API Server", "API 服务器"), + ("invalid_http", "必须以 http:// 或者 https:// 开头"), + ("Invalid IP", "无效 IP"), ("Invalid format", "无效格式"), ("server_not_support", "服务器暂不支持"), - ("Not available", "已被占用"), + ("Not available", "不可用"), ("Too frequent", "修改太频繁,请稍后再试"), ("Cancel", "取消"), ("Skip", "跳过"), @@ -72,12 +72,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter your password", "请输入密码"), ("Remember password", "记住密码"), ("Wrong Password", "密码错误"), - ("Do you want to enter again?", "还想输入一次吗?"), + ("Do you want to enter again?", "是否要再次输入?"), ("Connection Error", "连接错误"), ("Error", "错误"), ("Reset by the peer", "连接被对方关闭"), ("Connecting...", "正在连接..."), - ("Connection in progress. Please wait.", "连接进行中,请稍等。"), + ("Connection in progress. Please wait.", "正在进行连接,请稍候。"), ("Please try 1 minute later", "一分钟后再试"), ("Login Error", "登录错误"), ("Successful", "成功"), @@ -102,14 +102,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unselect All", "取消全选"), ("Empty Directory", "空文件夹"), ("Not an empty directory", "这不是一个空文件夹"), - ("Are you sure you want to delete this file?", "是否删除此文件?"), - ("Are you sure you want to delete this empty directory?", "是否删除此空文件夹?"), - ("Are you sure you want to delete the file of this directory?", "是否删除文件夹下的文件?"), + ("Are you sure you want to delete this file?", "是否删除此文件?"), + ("Are you sure you want to delete this empty directory?", "是否删除此空文件夹?"), + ("Are you sure you want to delete the file of this directory?", "是否删除此文件夹下的文件?"), ("Do this for all conflicts", "应用于其它冲突"), ("This is irreversible!", "此操作不可逆!"), ("Deleting", "正在删除"), ("files", "文件"), - ("Waiting", "等待..."), + ("Waiting", "正在等待..."), ("Finished", "完成"), ("Speed", "速度"), ("Custom Image Quality", "设置画面质量"), @@ -122,37 +122,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stretch", "伸展"), ("Scrollbar", "滚动条"), ("ScrollAuto", "自动滚动"), - ("Good image quality", "好画质"), - ("Balanced", "一般画质"), - ("Optimize reaction time", "优化反应时间"), + ("Good image quality", "画质最优化"), + ("Balanced", "平衡"), + ("Optimize reaction time", "速度最优化"), ("Custom", "自定义"), ("Show remote cursor", "显示远程光标"), ("Show quality monitor", "显示质量监测"), - ("Disable clipboard", "禁止剪贴板"), - ("Lock after session end", "断开后锁定远程电脑"), + ("Disable clipboard", "禁用剪贴板"), + ("Lock after session end", "会话结束后锁定远程电脑"), ("Insert", "插入"), ("Insert Lock", "锁定远程电脑"), ("Refresh", "刷新画面"), - ("ID does not exist", "ID不存在"), + ("ID does not exist", "ID 不存在"), ("Failed to connect to rendezvous server", "连接注册服务器失败"), ("Please try later", "请稍后再试"), - ("Remote desktop is offline", "远程电脑不在线"), - ("Key mismatch", "Key不匹配"), + ("Remote desktop is offline", "远程电脑处于离线状态"), + ("Key mismatch", "密钥不匹配"), ("Timeout", "连接超时"), ("Failed to connect to relay server", "无法连接到中继服务器"), ("Failed to connect via rendezvous server", "无法通过注册服务器建立连接"), ("Failed to connect via relay server", "无法通过中继服务器建立连接"), - ("Failed to make direct connection to remote desktop", "无法建立直接连接"), + ("Failed to make direct connection to remote desktop", "无法直接连接到远程桌面"), ("Set Password", "设置密码"), ("OS Password", "操作系统密码"), - ("install_tip", "你正在运行未安装版本,由于UAC限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将 RustDesk 安装到系统,从而规避上述问题。"), + ("install_tip", "你正在运行未安装版本,由于 UAC 限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将 RustDesk 安装到系统,从而规避上述问题。"), ("Click to upgrade", "点击这里升级"), ("Click to download", "点击这里下载"), ("Click to update", "点击这里更新"), ("Configure", "配置"), ("config_acc", "为了能够远程控制你的桌面, 请给予 RustDesk \"辅助功能\" 权限。"), ("config_screen", "为了能够远程访问你的桌面, 请给予 RustDesk \"屏幕录制\" 权限。"), - ("Installing ...", "安装 ..."), + ("Installing ...", "安装中..."), ("Install", "安装"), ("Installation", "安装"), ("Installation Path", "安装路径"), @@ -161,10 +161,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "开始安装即表示接受许可协议。"), ("Accept and Install", "同意并安装"), ("End-user license agreement", "用户协议"), - ("Generating ...", "正在产生 ..."), + ("Generating ...", "正在生成..."), ("Your installation is lower version.", "你安装的版本比当前运行的低。"), ("not_close_tcp_tip", "请在使用隧道的时候,不要关闭本窗口"), - ("Listening ...", "正在等待隧道连接 ..."), + ("Listening ...", "正在等待隧道连接..."), ("Remote Host", "远程主机"), ("Remote Port", "远程端口"), ("Action", "动作"), @@ -173,7 +173,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Address", "当前地址"), ("Change Local Port", "修改本地端口"), ("setup_server_tip", "如果需要更快连接速度,你可以选择自建服务器"), - ("Too short, at least 6 characters.", "太短了,至少6个字符"), + ("Too short, at least 6 characters.", "太短了,至少 6 个字符"), ("The confirmation is not identical.", "两次输入不匹配"), ("Permissions", "权限"), ("Accept", "接受"), @@ -183,21 +183,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Allow using clipboard", "允许使用剪贴板"), ("Allow hearing sound", "允许听到声音"), ("Allow file copy and paste", "允许复制粘贴文件"), - ("Connected", "已经连接"), + ("Connected", "已连接"), ("Direct and encrypted connection", "加密直连"), ("Relayed and encrypted connection", "加密中继连接"), ("Direct and unencrypted connection", "非加密直连"), ("Relayed and unencrypted connection", "非加密中继连接"), - ("Enter Remote ID", "输入对方ID"), + ("Enter Remote ID", "输入对方 ID"), ("Enter your password", "输入密码"), ("Logging in...", "正在登录..."), - ("Enable RDP session sharing", "允许RDP会话共享"), + ("Enable RDP session sharing", "允许 RDP 会话共享"), ("Auto Login", "自动登录(设置断开后锁定才有效)"), - ("Enable Direct IP Access", "允许IP直接访问"), - ("Rename", "改名"), + ("Enable Direct IP Access", "允许 IP 直接访问"), + ("Rename", "重命名"), ("Space", "空格"), ("Create Desktop Shortcut", "创建桌面快捷方式"), - ("Change Path", "改变路径"), + ("Change Path", "更改路径"), ("Create Folder", "创建文件夹"), ("Please enter the folder name", "请输入文件夹名称"), ("Fix it", "修复"), @@ -212,29 +212,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid port", "无效端口"), ("Closed manually by the peer", "被对方手动关闭"), ("Enable remote configuration modification", "允许远程修改配置"), - ("Run without install", "无安装运行"), + ("Run without install", "不安装直接运行"), ("Connect via relay", "中继连接"), ("Always connect via relay", "强制走中继连接"), - ("whitelist_tip", "只有白名单里的ip才能访问我"), + ("whitelist_tip", "只有白名单里的 IP 才能访问本机"), ("Login", "登录"), ("Verify", "验证"), ("Remember me", "记住我"), ("Trust this device", "信任此设备"), ("Verification code", "验证码"), - ("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,输入验证码继续登录"), + ("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,请输入验证码继续登录"), ("Logout", "登出"), ("Tags", "标签"), - ("Search ID", "查找ID"), + ("Search ID", "查找 ID"), ("whitelist_sep", "可以使用逗号,分号,空格或者换行符作为分隔符"), - ("Add ID", "增加ID"), + ("Add ID", "增加 ID"), ("Add Tag", "增加标签"), ("Unselect all tags", "取消选择所有标签"), ("Network error", "网络错误"), ("Username missed", "用户名没有填写"), ("Password missed", "密码没有填写"), - ("Wrong credentials", "提供的登入信息错误"), + ("Wrong credentials", "提供的登录信息错误"), ("Edit Tag", "修改标签"), - ("Unremember Password", "忘掉密码"), + ("Unremember Password", "忘记密码"), ("Favorites", "收藏"), ("Add to Favorites", "加入到收藏"), ("Remove from Favorites", "从收藏中删除"), @@ -244,9 +244,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hostname", "主机名"), ("Discovered", "已发现"), ("install_daemon_tip", "为了开机启动,请安装系统服务。"), - ("Remote ID", "远程ID"), + ("Remote ID", "远程 ID"), ("Paste", "粘贴"), - ("Paste here?", "粘贴到这里?"), + ("Paste here?", "粘贴到这里?"), ("Are you sure to close the connection?", "是否确认关闭连接?"), ("Download new version", "下载新版本"), ("Touch mode", "触屏模式"), @@ -284,7 +284,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "是否接受?"), ("Open System Setting", "打开系统设置"), ("How to get Android input permission?", "如何获取安卓的输入权限?"), - ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允許 RustDesk 使用\"无障碍\"服务。"), + ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允许 RustDesk 使用\"无障碍\"服务。"), ("android_input_permission_tip2", "请在接下来的系统设置页面里,找到并进入 [已安装的服务] 页面,将 [RustDesk Input] 服务开启。"), ("android_new_connection_tip", "收到新的连接控制请求,对方想要控制你当前的设备。"), ("android_service_will_start_tip", "开启录屏权限将自动开启服务,允许其他设备向此设备请求建立连接。"), @@ -293,7 +293,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_start_service_tip", "点击 [启动服务] 或打开 [屏幕录制] 权限开启手机屏幕共享服务。"), ("Account", "账户"), ("Overwrite", "覆盖"), - ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), + ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), ("Quit", "退出"), ("doc_mac_permission", "https://rustdesk.com/docs/zh-cn/manual/mac#%E5%90%AF%E7%94%A8%E6%9D%83%E9%99%90"), ("Help", "帮助"), @@ -314,7 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的 RustDesk 应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), - ("Map mode", "1:1传输"), + ("Map mode", "1:1 传输"), ("Translate mode", "翻译模式"), ("Use permanent password", "使用固定密码"), ("Use both passwords", "同时使用两种密码"), @@ -355,16 +355,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Audio", "允许传输音频"), ("Unlock Network Settings", "解锁网络设置"), ("Server", "服务器"), - ("Direct IP Access", "IP直接访问"), + ("Direct IP Access", "IP 直接访问"), ("Proxy", "代理"), ("Apply", "应用"), - ("Disconnect all devices?", "断开所有远程连接?"), + ("Disconnect all devices?", "断开所有远程连接?"), ("Clear", "清空"), ("Audio Input Device", "音频输入设备"), ("Deny remote access", "拒绝远程访问"), - ("Use IP Whitelisting", "只允许白名单上的IP访问"), + ("Use IP Whitelisting", "只允许白名单上的 IP 访问"), ("Network", "网络"), - ("Enable RDP", "允许RDP访问"), + ("Enable RDP", "允许 RDP 访问"), ("Pin menubar", "固定菜单栏"), ("Unpin menubar", "取消固定菜单栏"), ("Recording", "录屏"), @@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "拒绝局域网发现"), ("Write a message", "输入聊天消息"), ("Prompt", "提示"), - ("Please wait for confirmation of UAC...", "请等待对方确认 UAC ..."), + ("Please wait for confirmation of UAC...", "请等待对方确认 UAC..."), ("elevated_foreground_window_tip", "远端桌面的当前窗口需要更高的权限才能操作, 暂时无法使用鼠标键盘, 可以请求对方最小化当前窗口, 或者在连接管理窗口点击提升。为避免这个问题,建议在远端设备上安装本软件。"), ("Disconnected", "会话已结束"), ("Other", "其他"), @@ -396,7 +396,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "或"), ("Continue with", "使用"), ("Elevate", "提权"), - ("Zoom cursor", "缩放鼠标"), + ("Zoom cursor", "缩放光标"), ("Accept sessions via password", "只允许密码访问"), ("Accept sessions via click", "只允许点击访问"), ("Accept sessions via both", "允许密码或点击访问"), @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "请求访问你的设备"), ("Hide connection management window", "隐藏连接管理窗口"), ("hide_cm_tip", "在只允许密码连接并且只用固定密码的情况下才允许隐藏"), - ("wayland_experiment_tip", "Wayland 支持处于实验阶段,如果你需要使用无人值守访问,请使用X11。"), + ("wayland_experiment_tip", "Wayland 支持处于实验阶段,如果你需要使用无人值守访问,请使用 X11。"), ("Right click to select tabs", "右键选择选项卡"), ("Skipped", "已跳过"), ("Add to Address Book", "添加到地址簿"), @@ -417,7 +417,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local keyboard type", "本地键盘类型"), ("Select local keyboard type", "请选择本地键盘类型"), ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装 nouveau 驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), - ("Always use software rendering", "使用软件渲染"), + ("Always use software rendering", "始终使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), ("config_microphone", "为了支持通过麦克风进行音频传输,请给予 RustDesk \"录音\"权限。"), ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), @@ -434,25 +434,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("lowercase", "小写字母"), ("digit", "数字"), ("special character", "特殊字符"), - ("length>=8", "长度不小于8"), + ("length>=8", "长度不小于 8"), ("Weak", "弱"), ("Medium", "中"), ("Strong", "强"), ("Switch Sides", "反转访问方向"), - ("Please confirm if you want to share your desktop?", "请确认要让对方访问你的桌面?"), + ("Please confirm if you want to share your desktop?", "请确认是否要让对方访问你的桌面?"), ("Display", "显示"), ("Default View Style", "默认显示方式"), ("Default Scroll Style", "默认滚动方式"), ("Default Image Quality", "默认图像质量"), ("Default Codec", "默认编解码"), - ("Bitrate", "波特率"), + ("Bitrate", "码率"), ("FPS", "帧率"), ("Auto", "自动"), ("Other Default Options", "其它默认选项"), ("Voice call", "语音通话"), ("Text chat", "文字聊天"), - ("Stop voice call", "停止语音聊天"), - ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在ID后面添加/r,或者在卡片选项里选择强制走中继连接。"), + ("Stop voice call", "停止语音通话"), + ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在 ID 后面添加/r,或者在卡片选项里选择强制走中继连接。"), ("Reconnect", "重连"), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 00f6b70ac..70051f3e8 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -442,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), ("Display", "نمایش دادن"), ("Default View Style", "سبک نمایش پیش فرض"), - ("Default Scroll Style", "سبک پیش‌فرض اسکرول"), + ("Default Scroll Style", "سبک پیش‌ فرض اسکرول"), ("Default Image Quality", "کیفیت تصویر پیش فرض"), ("Default Codec", "کدک پیش فرض"), ("Bitrate", "میزان بیت صفحه نمایش"), @@ -452,7 +452,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "تماس صوتی"), ("Text chat", "گفتگو متنی (چت متنی)"), ("Stop voice call", "توقف تماس صوتی"), - ("relay_hint_tip", ""), - ("Reconnect", ""), + ("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), + ("Reconnect", "اتصال مجدد"), ].iter().cloned().collect(); } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 0c8c51455..910c26982 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -558,7 +558,7 @@ pub fn hide_dock() { } fn check_main_window() -> bool { - use sysinfo::{ProcessExt, System, SystemExt}; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; let mut sys = System::new(); sys.refresh_processes(); let app = format!("/Applications/{}.app", crate::get_app_name()); From c26053b8040f0cb13561f2bb42ce8b49f530901d Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 22 Feb 2023 23:17:33 +0100 Subject: [PATCH 160/202] formatted pubspec --- flutter/pubspec.yaml | 142 +++++++++++++++++++++---------------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index cd917b5e3..572b3e20a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -22,78 +22,78 @@ environment: sdk: ">=2.17.0" dependencies: - flutter: - sdk: flutter - flutter_localizations: - sdk: flutter + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.3 - ffi: ^2.0.1 - path_provider: ^2.0.12 - external_path: ^1.0.1 - provider: ^6.0.3 - tuple: ^2.0.0 - wakelock: ^0.6.2 - device_info_plus: ^4.1.2 - #firebase_analytics: ^9.1.5 - package_info_plus: ^1.4.2 - url_launcher: ^6.0.9 - toggle_switch: ^1.4.0 - dash_chat_2: ^0.0.14 - draggable_float_widget: ^0.0.2 - settings_ui: ^2.0.2 - flutter_breadcrumb: ^1.0.1 - http: ^0.13.4 - qr_code_scanner: ^1.0.0 - zxing2: ^0.1.0 - image_picker: ^0.8.5 - image: ^3.1.3 - back_button_interceptor: ^6.0.1 - flutter_rust_bridge: ^1.61.1 - window_manager: - git: - url: https://github.com/Kingtous/rustdesk_window_manager - ref: 32b24c66151b72bba033ef8b954486aa9351d97b - desktop_multi_window: - git: - url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a - freezed_annotation: ^2.0.3 - flutter_custom_cursor: ^0.0.4 - window_size: - git: - url: https://github.com/google/flutter-desktop-embedding.git - path: plugins/window_size - ref: a738913c8ce2c9f47515382d40827e794a334274 - get: ^4.6.5 - visibility_detector: ^0.3.3 - contextmenu: ^3.0.0 - desktop_drop: ^0.3.3 - scroll_pos: ^0.3.0 - debounce_throttle: ^2.0.0 - file_picker: ^5.1.0 - flutter_svg: ^1.1.5 - flutter_improved_scrolling: - # currently, we use flutter 3.0.5 for windows build, latest for other builds. - # - # for flutter 3.0.5, please use official version(just comment code below). - # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). - git: - url: https://github.com/Kingtous/flutter_improved_scrolling - ref: 62f09545149f320616467c306c8c5f71714a18e6 - uni_links: ^0.5.1 - uni_links_desktop: ^0.1.4 - path: ^1.8.1 - auto_size_text: ^3.0.0 - bot_toast: ^4.0.3 - win32: any - password_strength: ^0.2.0 - flutter_launcher_icons: ^0.11.0 - flutter_keyboard_visibility: ^5.4.0 - texture_rgba_renderer: ^0.0.8 - percent_indicator: ^4.2.2 + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.3 + ffi: ^2.0.1 + path_provider: ^2.0.12 + external_path: ^1.0.1 + provider: ^6.0.3 + tuple: ^2.0.0 + wakelock: ^0.6.2 + device_info_plus: ^4.1.2 + #firebase_analytics: ^9.1.5 + package_info_plus: ^1.4.2 + url_launcher: ^6.0.9 + toggle_switch: ^1.4.0 + dash_chat_2: ^0.0.14 + draggable_float_widget: ^0.0.2 + settings_ui: ^2.0.2 + flutter_breadcrumb: ^1.0.1 + http: ^0.13.4 + qr_code_scanner: ^1.0.0 + zxing2: ^0.1.0 + image_picker: ^0.8.5 + image: ^3.1.3 + back_button_interceptor: ^6.0.1 + flutter_rust_bridge: ^1.61.1 + window_manager: + git: + url: https://github.com/Kingtous/rustdesk_window_manager + ref: 32b24c66151b72bba033ef8b954486aa9351d97b + desktop_multi_window: + git: + url: https://github.com/Kingtous/rustdesk_desktop_multi_window + ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a + freezed_annotation: ^2.0.3 + flutter_custom_cursor: ^0.0.4 + window_size: + git: + url: https://github.com/google/flutter-desktop-embedding.git + path: plugins/window_size + ref: a738913c8ce2c9f47515382d40827e794a334274 + get: ^4.6.5 + visibility_detector: ^0.3.3 + contextmenu: ^3.0.0 + desktop_drop: ^0.3.3 + scroll_pos: ^0.3.0 + debounce_throttle: ^2.0.0 + file_picker: ^5.1.0 + flutter_svg: ^1.1.5 + flutter_improved_scrolling: + # currently, we use flutter 3.0.5 for windows build, latest for other builds. + # + # for flutter 3.0.5, please use official version(just comment code below). + # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). + git: + url: https://github.com/Kingtous/flutter_improved_scrolling + ref: 62f09545149f320616467c306c8c5f71714a18e6 + uni_links: ^0.5.1 + uni_links_desktop: ^0.1.4 + path: ^1.8.1 + auto_size_text: ^3.0.0 + bot_toast: ^4.0.3 + win32: any + password_strength: ^0.2.0 + flutter_launcher_icons: ^0.11.0 + flutter_keyboard_visibility: ^5.4.0 + texture_rgba_renderer: ^0.0.8 + percent_indicator: ^4.2.2 dev_dependencies: icons_launcher: ^2.0.4 From 30840f9988a90d3000910da377e46b17301de03f Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 14:43:41 +0800 Subject: [PATCH 161/202] fix toggle clipboard dead lock Signed-off-by: fufesou --- src/client.rs | 5 ----- src/flutter_ffi.rs | 10 ++++++++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/client.rs b/src/client.rs index f36bdae78..aa3523185 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1241,11 +1241,6 @@ impl LoginConfigHandler { if !name.contains("block-input") { self.save_config(config); } - #[cfg(feature = "flutter")] - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if name == "disable-clipboard" { - crate::flutter::update_text_clipboard_required(); - } let mut misc = Misc::new(); misc.set_option(option); let mut msg_out = Message::new(); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 68ddce9b7..7eeb96b5c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -165,9 +165,15 @@ pub fn session_reconnect(id: String, force_relay: bool) { } pub fn session_toggle_option(id: String, value: String) { + let mut is_found = false; if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { - log::warn!("toggle option {}", value); - session.toggle_option(value); + is_found = true; + log::warn!("toggle option {}", &value); + session.toggle_option(value.clone()); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if is_found && value == "disable-clipboard" { + crate::flutter::update_text_clipboard_required(); } } From 54bebee35fef2f2c2746a0ddfcb3f9bab5badfc5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 18 Feb 2023 11:16:07 +0800 Subject: [PATCH 162/202] wip: texture windows --- Cargo.lock | 1 + Cargo.toml | 3 ++- src/flutter.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 115845b50..eb5461a6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4860,6 +4860,7 @@ dependencies = [ "include_dir", "jni 0.19.0", "lazy_static", + "libloading", "libpulse-binding", "libpulse-simple-binding", "mac_address", diff --git a/Cargo.toml b/Cargo.toml index f685e3f2e..0a7af0cbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ default-run = "rustdesk" [lib] name = "librustdesk" -crate-type = ["cdylib", "staticlib", "rlib"] +crate-type = ["cdylib"] [[bin]] name = "naming" @@ -67,6 +67,7 @@ url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } chrono = "0.4.23" cidr-utils = "0.5.9" +libloading = "0.7.4" [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.13.5" diff --git a/src/flutter.rs b/src/flutter.rs index bad6e0008..cab7a900d 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -8,6 +8,8 @@ use hbb_common::{ bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, ResultType, }; +use libc::c_void; +use libloading::Library; use serde_json::json; use std::sync::atomic::{AtomicBool, Ordering}; use std::{ @@ -115,6 +117,58 @@ pub struct FlutterHandler { // We must check the `rgba_valid` before reading [rgba]. pub rgba: Arc>>, pub rgba_valid: Arc, + pub renderer: Arc> +} +// pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(texture_rgba: *mut c_void , buffer: *const u8 , width: c_int, height: c_int); + +extern "C" { + fn FlutterRgbaRendererPluginOnRgba(texture_rgba: *mut c_void , buffer: *const u8 , width: c_int, height: c_int); +} + +// Video Texture Renderer in Flutter +#[derive(Default, Clone)] +pub struct VideoRenderer { + // TextureRgba pointer in flutter native. + ptr: usize, + width: i32, + height: i32, + // on_rgba_func: FlutterRgbaRendererPluginOnRgba +} + +// impl Default for VideoRenderer { +// fn default() -> Self { +// unsafe { +// let lib = Library::new("texture_rgba_renderer_plugin").expect("`libtexture_rgba_renderer_plugin` not found, please add `texture_rgba_renderer` in your project"); +// let func = lib.get(b"FlutterRgbaRendererPluginOnRgba"); +// } + +// } +// } + + + +impl VideoRenderer { + pub fn new(ptr: usize) -> Self { + Self { + ptr, + ..Default::default() + } + } + + pub fn set_size(&mut self, width: i32, height: i32) { + self.width = width; + self.height = height; + } + + pub fn on_rgba(&self, rgba: *const u8) { + if self.ptr == usize::default() { + return; + } + #[cfg(target_os = "windows")] + unsafe { + FlutterRgbaRendererPluginOnRgba(self.ptr as _, rgba, self.width as _, self.height as _); + } + } } impl FlutterHandler { @@ -156,6 +210,10 @@ impl FlutterHandler { } serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) } + + pub fn register_texture(&self, ptr: usize) { + self.renderer.write().unwrap().ptr = ptr; + } } impl InvokeUiSession for FlutterHandler { @@ -324,9 +382,12 @@ impl InvokeUiSession for FlutterHandler { self.rgba_valid.store(true, Ordering::Relaxed); // Return the rgba buffer to the video handler for reusing allocated rgba buffer. std::mem::swap::>(data, &mut *self.rgba.write().unwrap()); + #[cfg(not(any(target_os = "windows")))] if let Some(stream) = &*self.event_stream.read().unwrap() { stream.add(EventToUI::Rgba); } + #[cfg(any(target_os = "windows"))] + self.renderer.read().unwrap().on_rgba(self.rgba.read().unwrap().as_ptr()); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -741,3 +802,13 @@ pub fn session_next_rgba(id: *const char) { } } } + +#[no_mangle] +pub fn session_register_texture(id: *const char, ptr: usize) { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + return session.register_texture(ptr); + } + } +} \ No newline at end of file From ea07b9690e8cc3ae7e7c8e88dd01a50117e789e6 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 18 Feb 2023 11:47:18 +0800 Subject: [PATCH 163/202] fix: rgba compile --- src/flutter.rs | 69 ++++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index cab7a900d..2888ffe75 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -9,7 +9,7 @@ use hbb_common::{ ResultType, }; use libc::c_void; -use libloading::Library; +use libloading::{Library, Symbol}; use serde_json::json; use std::sync::atomic::{AtomicBool, Ordering}; use std::{ @@ -29,6 +29,18 @@ lazy_static::lazy_static! { pub static ref CUR_SESSION_ID: RwLock = Default::default(); pub static ref SESSIONS: RwLock>> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel + #[cfg(not(any(target_os = "ios", target_os = "android")))] + pub static ref TEXURE_RGBA_RENDERER_PLUGIN: Library = { + unsafe { + #[cfg(target_os = "windows")] + let lib = Library::new("texture_rgba_renderer_plugin.dll"); + #[cfg(target_os = "macos")] + let lib = Library::new("texture_rgba_renderer_plugin.dylib"); + #[cfg(target_os = "linux")] + let lib = Library::new("texture_rgba_renderer_plugin.so"); + lib.expect("`libtexture_rgba_renderer_plugin` not found, please add `texture_rgba_renderer` in your flutter project") + } + }; } /// FFI for rustdesk core's main entry. @@ -117,35 +129,33 @@ pub struct FlutterHandler { // We must check the `rgba_valid` before reading [rgba]. pub rgba: Arc>>, pub rgba_valid: Arc, - pub renderer: Arc> -} -// pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(texture_rgba: *mut c_void , buffer: *const u8 , width: c_int, height: c_int); - -extern "C" { - fn FlutterRgbaRendererPluginOnRgba(texture_rgba: *mut c_void , buffer: *const u8 , width: c_int, height: c_int); + pub renderer: VideoRenderer, } +pub type FlutterRgbaRendererPluginOnRgba = + unsafe extern "C" fn(texture_rgba: *mut c_void, buffer: *const u8, width: c_int, height: c_int); // Video Texture Renderer in Flutter -#[derive(Default, Clone)] +#[derive(Clone)] pub struct VideoRenderer { // TextureRgba pointer in flutter native. ptr: usize, width: i32, height: i32, - // on_rgba_func: FlutterRgbaRendererPluginOnRgba + on_rgba_func: Symbol<'static, FlutterRgbaRendererPluginOnRgba>, } -// impl Default for VideoRenderer { -// fn default() -> Self { -// unsafe { -// let lib = Library::new("texture_rgba_renderer_plugin").expect("`libtexture_rgba_renderer_plugin` not found, please add `texture_rgba_renderer` in your project"); -// let func = lib.get(b"FlutterRgbaRendererPluginOnRgba"); -// } - -// } -// } - - +impl Default for VideoRenderer { + fn default() -> Self { + unsafe { + Self { + on_rgba_func: TEXURE_RGBA_RENDERER_PLUGIN + .get::(b"FlutterRgbaRendererPluginOnRgba") + .expect("Symbol FlutterRgbaRendererPluginOnRgba not found."), + ..Default::default() + } + } + } +} impl VideoRenderer { pub fn new(ptr: usize) -> Self { @@ -164,10 +174,8 @@ impl VideoRenderer { if self.ptr == usize::default() { return; } - #[cfg(target_os = "windows")] - unsafe { - FlutterRgbaRendererPluginOnRgba(self.ptr as _, rgba, self.width as _, self.height as _); - } + let func = self.on_rgba_func.clone(); + unsafe {func(self.ptr as _, rgba, self.width as _, self.height as _)}; } } @@ -211,8 +219,8 @@ impl FlutterHandler { serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) } - pub fn register_texture(&self, ptr: usize) { - self.renderer.write().unwrap().ptr = ptr; + pub fn register_texture(&mut self, ptr: usize) { + self.renderer.ptr = ptr; } } @@ -382,12 +390,13 @@ impl InvokeUiSession for FlutterHandler { self.rgba_valid.store(true, Ordering::Relaxed); // Return the rgba buffer to the video handler for reusing allocated rgba buffer. std::mem::swap::>(data, &mut *self.rgba.write().unwrap()); - #[cfg(not(any(target_os = "windows")))] + #[cfg(any(target_os = "android", target_os = "ios"))] if let Some(stream) = &*self.event_stream.read().unwrap() { stream.add(EventToUI::Rgba); } - #[cfg(any(target_os = "windows"))] - self.renderer.read().unwrap().on_rgba(self.rgba.read().unwrap().as_ptr()); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.renderer + .on_rgba(self.rgba.read().unwrap().as_ptr()); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -811,4 +820,4 @@ pub fn session_register_texture(id: *const char, ptr: usize) { return session.register_texture(ptr); } } -} \ No newline at end of file +} From d3455f3ce2711e8af6631df25511f28548278720 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 19 Feb 2023 15:25:30 +0800 Subject: [PATCH 164/202] feat: adapt for the latest renderer plugin --- flutter/lib/common.dart | 5 +++ flutter/lib/desktop/pages/remote_page.dart | 42 ++++++++++++++++++---- flutter/lib/models/model.dart | 17 ++++----- flutter/lib/models/native_model.dart | 13 +++++++ src/flutter.rs | 28 ++++++++++----- src/ui/remote.rs | 2 +- src/ui_session_interface.rs | 2 +- 7 files changed, 85 insertions(+), 24 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ff8dfbb09..6d3e4c3b7 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -19,6 +19,7 @@ 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:uni_links_desktop/uni_links_desktop.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -44,6 +45,10 @@ var isWeb = false; var isWebDesktop = false; 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; diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index f9db985d9..df9874172 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -63,6 +63,8 @@ class _RemotePageState extends State late RxBool _zoomCursor; late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; + late RxInt _textureId; + late int _textureKey; final _blockableOverlayState = BlockableOverlayState(); @@ -85,6 +87,8 @@ class _RemotePageState extends State _showRemoteCursor = ShowRemoteCursorState.find(id); _keyboardEnabled = KeyboardEnabledState.find(id); _remoteCursorMoved = RemoteCursorMovedState.find(id); + _textureKey = newTextureId; + _textureId = RxInt(-1); } void _removeStates(String id) { @@ -119,6 +123,16 @@ class _RemotePageState extends State if (!Platform.isLinux) { Wakelock.enable(); } + // Register texture. + _textureId.value = -1; + textureRenderer.createTexture(_textureKey).then((id) async { + if (id != -1) { + final ptr = await textureRenderer.getTexturePtr(_textureKey); + debugPrint("id: $id, texture_key: $_textureKey"); + platformFFI.registerTexture(widget.id, ptr); + _textureId.value = id; + } + }); _ffi.ffiModel.updateEventListener(widget.id); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // Session option should be set after models.dart/FFI.start @@ -198,6 +212,7 @@ class _RemotePageState extends State Wakelock.disable(); } Get.delete(tag: widget.id); + textureRenderer.closeTexture(_textureKey); super.dispose(); _removeStates(widget.id); } @@ -346,6 +361,7 @@ class _RemotePageState extends State cursorOverImage: _cursorOverImage, keyboardEnabled: _keyboardEnabled, remoteCursorMoved: _remoteCursorMoved, + textureId: _textureId, listenerBuilder: (child) => _buildRawPointerMouseRegion(child, enterView, leaveView), ); @@ -383,6 +399,7 @@ class ImagePaint extends StatefulWidget { final RxBool cursorOverImage; final RxBool keyboardEnabled; final RxBool remoteCursorMoved; + final RxInt textureId; final Widget Function(Widget)? listenerBuilder; ImagePaint( @@ -392,6 +409,7 @@ class ImagePaint extends StatefulWidget { required this.cursorOverImage, required this.keyboardEnabled, required this.remoteCursorMoved, + required this.textureId, this.listenerBuilder}) : super(key: key); @@ -466,9 +484,15 @@ class _ImagePaintState extends State { final imageWidth = c.getDisplayWidth() * s; final imageHeight = c.getDisplayHeight() * s; final imageSize = Size(imageWidth, imageHeight); - final imageWidget = CustomPaint( - size: imageSize, - painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + print("width: $imageWidth/$imageHeight"); + // final imageWidget = CustomPaint( + // size: imageSize, + // painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + // ); + final imageWidget = SizedBox( + width: imageHeight, + height: imageHeight, + child: Obx(() => Texture(textureId: widget.textureId.value)), ); return NotificationListener( @@ -493,9 +517,15 @@ class _ImagePaintState extends State { context, _buildListener(imageWidget), c.size, imageSize)), )); } else { - final imageWidget = CustomPaint( - size: Size(c.size.width, c.size.height), - painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + // final imageWidget = CustomPaint( + // size: Size(c.size.width, c.size.height), + // painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + // ); + final imageWidget = Center( + child: AspectRatio( + aspectRatio: c.size.width / c.size.height, + child: Obx(() => Texture(textureId: widget.textureId.value)), + ), ); return mouseRegion(child: _buildListener(imageWidget)); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1afb5b147..0b6f14636 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1446,14 +1446,15 @@ class FFI { } } else if (message is EventToUI_Rgba) { // Fetch the image buffer from rust codes. - final sz = platformFFI.getRgbaSize(id); - if (sz == null || sz == 0) { - return; - } - final rgba = platformFFI.getRgba(id, sz); - if (rgba != null) { - imageModel.onRgba(rgba); - } + // final sz = platformFFI.getRgbaSize(id); + // if (sz == null || sz == 0) { + // return; + // } + // final rgba = platformFFI.getRgba(id, sz); + // if (rgba != null) { + // imageModel.onRgba(rgba); + // } + // imageModel.onRgba(rgba); } } debugPrint('Exit session event loop'); diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index ba62b775e..13f5b4587 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -30,6 +30,9 @@ typedef F4Dart = int Function(Pointer); typedef F5 = Void Function(Pointer); typedef F5Dart = void Function(Pointer); typedef HandleEvent = Future Function(Map evt); +// pub fn session_register_texture(id: *const char, ptr: usize) +typedef F6 = Void Function(Pointer, Uint64); +typedef F6Dart = void Function(Pointer, int); /// FFI wrapper around the native Rust core. /// Hides the platform differences. @@ -52,6 +55,8 @@ class PlatformFFI { F3? _session_get_rgba; F4Dart? _session_get_rgba_size; F5Dart? _session_next_rgba; + F6Dart? _session_register_texture; + static get localeName => Platform.localeName; @@ -130,6 +135,13 @@ class PlatformFFI { malloc.free(a); } + void registerTexture(String id, int ptr) { + if (_session_register_texture == null) return; + final a = id.toNativeUtf8(); + _session_register_texture!(a, ptr); + malloc.free(a); + } + /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { _appType = appType; @@ -150,6 +162,7 @@ class PlatformFFI { dylib.lookupFunction("session_get_rgba_size"); _session_next_rgba = dylib.lookupFunction("session_next_rgba"); + _session_register_texture = dylib.lookupFunction("session_register_texture"); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; diff --git a/src/flutter.rs b/src/flutter.rs index 2888ffe75..f5d764e66 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -8,7 +8,7 @@ use hbb_common::{ bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, ResultType, }; -use libc::c_void; +use libc::{c_void}; use libloading::{Library, Symbol}; use serde_json::json; use std::sync::atomic::{AtomicBool, Ordering}; @@ -37,7 +37,7 @@ lazy_static::lazy_static! { #[cfg(target_os = "macos")] let lib = Library::new("texture_rgba_renderer_plugin.dylib"); #[cfg(target_os = "linux")] - let lib = Library::new("texture_rgba_renderer_plugin.so"); + let lib = Library::new("libtexture_rgba_renderer_plugin.so"); lib.expect("`libtexture_rgba_renderer_plugin` not found, please add `texture_rgba_renderer` in your flutter project") } }; @@ -129,7 +129,8 @@ pub struct FlutterHandler { // We must check the `rgba_valid` before reading [rgba]. pub rgba: Arc>>, pub rgba_valid: Arc, - pub renderer: VideoRenderer, + pub renderer: Arc>, + peer_info: Arc> } pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(texture_rgba: *mut c_void, buffer: *const u8, width: c_int, height: c_int); @@ -148,10 +149,12 @@ impl Default for VideoRenderer { fn default() -> Self { unsafe { Self { + ptr: 0, + width: 0, + height: 0, on_rgba_func: TEXURE_RGBA_RENDERER_PLUGIN .get::(b"FlutterRgbaRendererPluginOnRgba") .expect("Symbol FlutterRgbaRendererPluginOnRgba not found."), - ..Default::default() } } } @@ -220,7 +223,7 @@ impl FlutterHandler { } pub fn register_texture(&mut self, ptr: usize) { - self.renderer.ptr = ptr; + self.renderer.write().unwrap().ptr = ptr; } } @@ -381,6 +384,7 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter fn adapt_size(&self) {} + #[inline] fn on_rgba(&self, data: &mut Vec) { // If the current rgba is not fetched by flutter, i.e., is valid. // We give up sending a new event to flutter. @@ -390,13 +394,15 @@ impl InvokeUiSession for FlutterHandler { self.rgba_valid.store(true, Ordering::Relaxed); // Return the rgba buffer to the video handler for reusing allocated rgba buffer. std::mem::swap::>(data, &mut *self.rgba.write().unwrap()); - #[cfg(any(target_os = "android", target_os = "ios"))] if let Some(stream) = &*self.event_stream.read().unwrap() { stream.add(EventToUI::Rgba); } #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.renderer + { + self.renderer.read().unwrap() .on_rgba(self.rgba.read().unwrap().as_ptr()); + self.next_rgba(); + } } fn set_peer_info(&self, pi: &PeerInfo) { @@ -410,6 +416,9 @@ impl InvokeUiSession for FlutterHandler { features.insert("privacy_mode", 0); } let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); + *self.peer_info.write().unwrap() = pi.clone(); + let curr_display = &pi.displays[pi.current_display as usize]; + self.renderer.write().unwrap().set_size(curr_display.width, curr_display.height); self.push_event( "peer_info", vec![ @@ -426,6 +435,7 @@ impl InvokeUiSession for FlutterHandler { } fn set_displays(&self, displays: &Vec) { + self.peer_info.write().unwrap().displays = displays.clone(); self.push_event( "sync_peer_info", vec![("displays", &Self::make_displays_msg(displays))], @@ -457,6 +467,8 @@ impl InvokeUiSession for FlutterHandler { } fn switch_display(&self, display: &SwitchDisplay) { + let curr_display = &self.peer_info.read().unwrap().displays[display.display as usize]; + self.renderer.write().unwrap().set_size(curr_display.width, curr_display.height); self.push_event( "switch_display", vec![ @@ -521,7 +533,7 @@ impl InvokeUiSession for FlutterHandler { } #[inline] - fn next_rgba(&mut self) { + fn next_rgba(&self) { self.rgba_valid.store(false, Ordering::Relaxed); } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 4794efb65..7b31c84e9 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -298,7 +298,7 @@ impl InvokeUiSession for SciterHandler { std::ptr::null() } - fn next_rgba(&mut self) {} + fn next_rgba(&self) {} } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 5a83ee572..5fbf2f4e7 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -798,7 +798,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); fn get_rgba(&self) -> *const u8; - fn next_rgba(&mut self); + fn next_rgba(&self); } impl Deref for Session { From 5acedecf0c09546ec368ca22fa7367ec7b9c0ae5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 21 Feb 2023 21:56:46 +0800 Subject: [PATCH 165/202] texture paint Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 33 ++++++--- flutter/lib/models/model.dart | 45 +++++++---- src/flutter.rs | 86 ++++++++++++++-------- src/flutter_ffi.rs | 6 ++ 4 files changed, 115 insertions(+), 55 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index df9874172..4a2f5c0e8 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -126,9 +126,9 @@ class _RemotePageState extends State // Register texture. _textureId.value = -1; textureRenderer.createTexture(_textureKey).then((id) async { + debugPrint("id: $id, texture_key: $_textureKey"); if (id != -1) { final ptr = await textureRenderer.getTexturePtr(_textureKey); - debugPrint("id: $id, texture_key: $_textureKey"); platformFFI.registerTexture(widget.id, ptr); _textureId.value = id; } @@ -197,6 +197,8 @@ class _RemotePageState extends State @override void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); + platformFFI.registerTexture(widget.id, 0); + textureRenderer.closeTexture(_textureKey); // ensure we leave this session, this is a double check bind.sessionEnterOrLeave(id: widget.id, enter: false); DesktopMultiWindow.removeListener(this); @@ -212,7 +214,6 @@ class _RemotePageState extends State Wakelock.disable(); } Get.delete(tag: widget.id); - textureRenderer.closeTexture(_textureKey); super.dispose(); _removeStates(widget.id); } @@ -484,15 +485,14 @@ class _ImagePaintState extends State { final imageWidth = c.getDisplayWidth() * s; final imageHeight = c.getDisplayHeight() * s; final imageSize = Size(imageWidth, imageHeight); - print("width: $imageWidth/$imageHeight"); // final imageWidget = CustomPaint( // size: imageSize, // painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), // ); final imageWidget = SizedBox( - width: imageHeight, + width: imageWidth, height: imageHeight, - child: Obx(() => Texture(textureId: widget.textureId.value)), + child: Obx(() => Texture(textureId: widget.textureId.value)), ); return NotificationListener( @@ -521,13 +521,22 @@ class _ImagePaintState extends State { // size: Size(c.size.width, c.size.height), // painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), // ); - final imageWidget = Center( - child: AspectRatio( - aspectRatio: c.size.width / c.size.height, - child: Obx(() => Texture(textureId: widget.textureId.value)), - ), - ); - return mouseRegion(child: _buildListener(imageWidget)); + if (c.size.width > 0 && c.size.height > 0) { + final imageWidget = Stack( + children: [ + Positioned( + left: c.x, + top: c.y, + width: c.getDisplayWidth() * s, + height: c.getDisplayHeight() * s, + child: Texture(textureId: widget.textureId.value), + ) + ], + ); + return mouseRegion(child: _buildListener(imageWidget)); + } else { + return Container(); + } } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 0b6f14636..a38db2a90 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -252,6 +252,8 @@ class FfiModel with ChangeNotifier { parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); } + _updateSessionWidthHeight(peerId, display.width, display.height); + try { CurrentDisplayState.find(peerId).value = _pi.currentDisplay; } catch (e) { @@ -367,6 +369,10 @@ class FfiModel with ChangeNotifier { }); } + _updateSessionWidthHeight(String id, int width, int height) { + bind.sessionSetSize(id: id, width: display.width, height: display.height); + } + /// Handle the peer info event based on [evt]. handlePeerInfo(Map evt, String peerId) async { // recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs) @@ -420,6 +426,7 @@ class FfiModel with ChangeNotifier { stateGlobal.displaysCount.value = _pi.displays.length; if (_pi.currentDisplay < _pi.displays.length) { _display = _pi.displays[_pi.currentDisplay]; + _updateSessionWidthHeight(peerId, display.width, display.height); } if (displays.isNotEmpty) { parent.target?.dialogManager.showLoading( @@ -485,19 +492,18 @@ class ImageModel with ChangeNotifier { WeakReference parent; - final List _callbacksOnFirstImage = []; + final List callbacksOnFirstImage = []; ImageModel(this.parent); - addCallbackOnFirstImage(Function(String) cb) => - _callbacksOnFirstImage.add(cb); + addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb); onRgba(Uint8List rgba) { if (_waitForImage[id]!) { _waitForImage[id] = false; parent.target?.dialogManager.dismissAll(); if (isDesktop) { - for (final cb in _callbacksOnFirstImage) { + for (final cb in callbacksOnFirstImage) { cb(id); } } @@ -1445,16 +1451,27 @@ class FFI { debugPrint('json.decode fail1(): $e, ${message.field0}'); } } else if (message is EventToUI_Rgba) { - // Fetch the image buffer from rust codes. - // final sz = platformFFI.getRgbaSize(id); - // if (sz == null || sz == 0) { - // return; - // } - // final rgba = platformFFI.getRgba(id, sz); - // if (rgba != null) { - // imageModel.onRgba(rgba); - // } - // imageModel.onRgba(rgba); + if (Platform.isAndroid || Platform.isIOS) { + // Fetch the image buffer from rust codes. + final sz = platformFFI.getRgbaSize(id); + if (sz == null || sz == 0) { + return; + } + final rgba = platformFFI.getRgba(id, sz); + if (rgba != null) { + imageModel.onRgba(rgba); + } + } else { + if (_waitForImage[id]!) { + _waitForImage[id] = false; + dialogManager.dismissAll(); + for (final cb in imageModel.callbacksOnFirstImage) { + cb(id); + } + await canvasModel.updateViewStyle(); + await canvasModel.updateScrollStyle(); + } + } } } debugPrint('Exit session event loop'); diff --git a/src/flutter.rs b/src/flutter.rs index f5d764e66..a5689bce6 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -5,12 +5,13 @@ use crate::{ }; use flutter_rust_bridge::StreamSink; use hbb_common::{ - bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, - ResultType, + bail, config::LocalConfig, get_version_number, libc::c_void, message_proto::*, + rendezvous_proto::ConnType, ResultType, }; -use libc::{c_void}; use libloading::{Library, Symbol}; use serde_json::json; + +#[cfg(any(target_os = "android", target_os = "ios"))] use std::sync::atomic::{AtomicBool, Ordering}; use std::{ collections::HashMap, @@ -30,7 +31,7 @@ lazy_static::lazy_static! { pub static ref SESSIONS: RwLock>> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel #[cfg(not(any(target_os = "ios", target_os = "android")))] - pub static ref TEXURE_RGBA_RENDERER_PLUGIN: Library = { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Library = { unsafe { #[cfg(target_os = "windows")] let lib = Library::new("texture_rgba_renderer_plugin.dll"); @@ -127,21 +128,26 @@ pub struct FlutterHandler { pub event_stream: Arc>>>, // SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`. // We must check the `rgba_valid` before reading [rgba]. + #[cfg(any(target_os = "android", target_os = "ios"))] pub rgba: Arc>>, + #[cfg(any(target_os = "android", target_os = "ios"))] pub rgba_valid: Arc, - pub renderer: Arc>, - peer_info: Arc> + #[cfg(not(any(target_os = "android", target_os = "ios")))] + notify_rendered: Arc>, + renderer: Arc>, + peer_info: Arc>, } pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(texture_rgba: *mut c_void, buffer: *const u8, width: c_int, height: c_int); // Video Texture Renderer in Flutter #[derive(Clone)] -pub struct VideoRenderer { +struct VideoRenderer { // TextureRgba pointer in flutter native. ptr: usize, width: i32, height: i32, + data_len: usize, on_rgba_func: Symbol<'static, FlutterRgbaRendererPluginOnRgba>, } @@ -152,7 +158,8 @@ impl Default for VideoRenderer { ptr: 0, width: 0, height: 0, - on_rgba_func: TEXURE_RGBA_RENDERER_PLUGIN + data_len: 0, + on_rgba_func: TEXTURE_RGBA_RENDERER_PLUGIN .get::(b"FlutterRgbaRendererPluginOnRgba") .expect("Symbol FlutterRgbaRendererPluginOnRgba not found."), } @@ -161,24 +168,30 @@ impl Default for VideoRenderer { } impl VideoRenderer { - pub fn new(ptr: usize) -> Self { - Self { - ptr, - ..Default::default() - } - } - + #[inline] pub fn set_size(&mut self, width: i32, height: i32) { self.width = width; self.height = height; + self.data_len = if width > 0 && height > 0 { + (width * height * 4) as usize + } else { + 0 + }; } - pub fn on_rgba(&self, rgba: *const u8) { - if self.ptr == usize::default() { + pub fn on_rgba(&self, rgba: &Vec) { + if self.ptr == usize::default() || rgba.len() != self.data_len { return; } let func = self.on_rgba_func.clone(); - unsafe {func(self.ptr as _, rgba, self.width as _, self.height as _)}; + unsafe { + func( + self.ptr as _, + rgba.as_ptr() as _, + self.width as _, + self.height as _, + ) + }; } } @@ -222,9 +235,16 @@ impl FlutterHandler { serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) } + #[inline] pub fn register_texture(&mut self, ptr: usize) { self.renderer.write().unwrap().ptr = ptr; } + + #[inline] + pub fn set_size(&mut self, width: i32, height: i32) { + *self.notify_rendered.write().unwrap() = false; + self.renderer.write().unwrap().set_size(width, height); + } } impl InvokeUiSession for FlutterHandler { @@ -385,6 +405,7 @@ impl InvokeUiSession for FlutterHandler { fn adapt_size(&self) {} #[inline] + #[cfg(any(target_os = "android", target_os = "ios"))] fn on_rgba(&self, data: &mut Vec) { // If the current rgba is not fetched by flutter, i.e., is valid. // We give up sending a new event to flutter. @@ -397,11 +418,18 @@ impl InvokeUiSession for FlutterHandler { if let Some(stream) = &*self.event_stream.read().unwrap() { stream.add(EventToUI::Rgba); } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - self.renderer.read().unwrap() - .on_rgba(self.rgba.read().unwrap().as_ptr()); - self.next_rgba(); + } + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn on_rgba(&self, data: &mut Vec) { + self.renderer.read().unwrap().on_rgba(data); + if *self.notify_rendered.read().unwrap() { + return; + } + if let Some(stream) = &*self.event_stream.read().unwrap() { + stream.add(EventToUI::Rgba); + *self.notify_rendered.write().unwrap() = true; } } @@ -417,8 +445,6 @@ impl InvokeUiSession for FlutterHandler { } let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); *self.peer_info.write().unwrap() = pi.clone(); - let curr_display = &pi.displays[pi.current_display as usize]; - self.renderer.write().unwrap().set_size(curr_display.width, curr_display.height); self.push_event( "peer_info", vec![ @@ -467,8 +493,6 @@ impl InvokeUiSession for FlutterHandler { } fn switch_display(&self, display: &SwitchDisplay) { - let curr_display = &self.peer_info.read().unwrap().displays[display.display as usize]; - self.renderer.write().unwrap().set_size(curr_display.width, curr_display.height); self.push_event( "switch_display", vec![ @@ -526,6 +550,7 @@ impl InvokeUiSession for FlutterHandler { #[inline] fn get_rgba(&self) -> *const u8 { + #[cfg(any(target_os = "android", target_os = "ios"))] if self.rgba_valid.load(Ordering::Relaxed) { return self.rgba.read().unwrap().as_ptr(); } @@ -534,6 +559,7 @@ impl InvokeUiSession for FlutterHandler { #[inline] fn next_rgba(&self) { + #[cfg(any(target_os = "android", target_os = "ios"))] self.rgba_valid.store(false, Ordering::Relaxed); } } @@ -793,8 +819,10 @@ pub fn set_cur_session_id(id: String) { } #[no_mangle] -pub fn session_get_rgba_size(id: *const char) -> usize { - let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; +pub fn session_get_rgba_size(_id: *const char) -> usize { + #[cfg(any(target_os = "android", target_os = "ios"))] + let id = unsafe { std::ffi::CStr::from_ptr(_id as _) }; + #[cfg(any(target_os = "android", target_os = "ios"))] if let Ok(id) = id.to_str() { if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { return session.rgba.read().unwrap().len(); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 7eeb96b5c..c55866dbe 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -529,6 +529,12 @@ pub fn session_switch_sides(id: String) { } } +pub fn session_set_size(id: String, width: i32, height: i32) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.set_size(width, height); + } +} + pub fn main_get_sound_inputs() -> Vec { #[cfg(not(any(target_os = "android", target_os = "ios")))] return get_sound_inputs(); From 77c4a14845604775751359b51cc5be2b8ae90c23 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 21 Feb 2023 23:46:13 +0800 Subject: [PATCH 166/202] flutter texture render, mid commit Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 61 ++++---- flutter/lib/models/model.dart | 23 +-- libs/scrap/src/common/codec.rs | 26 ++-- libs/scrap/src/common/convert.rs | 158 ++++++++++++++++----- libs/scrap/src/common/hwcodec.rs | 22 ++- libs/scrap/src/common/mediacodec.rs | 52 +++++-- libs/scrap/src/common/mod.rs | 7 + libs/scrap/src/common/vpxcodec.rs | 82 +++++++---- src/client.rs | 7 +- src/flutter.rs | 16 ++- src/flutter_ffi.rs | 11 ++ 11 files changed, 322 insertions(+), 143 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 4a2f5c0e8..c78ffb439 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -65,6 +65,7 @@ class _RemotePageState extends State late RxBool _keyboardEnabled; late RxInt _textureId; late int _textureKey; + final useTextureRender = bind.mainUseTextureRender(); final _blockableOverlayState = BlockableOverlayState(); @@ -363,6 +364,7 @@ class _RemotePageState extends State keyboardEnabled: _keyboardEnabled, remoteCursorMoved: _remoteCursorMoved, textureId: _textureId, + useTextureRender: useTextureRender, listenerBuilder: (child) => _buildRawPointerMouseRegion(child, enterView, leaveView), ); @@ -401,6 +403,7 @@ class ImagePaint extends StatefulWidget { final RxBool keyboardEnabled; final RxBool remoteCursorMoved; final RxInt textureId; + final bool useTextureRender; final Widget Function(Widget)? listenerBuilder; ImagePaint( @@ -411,6 +414,7 @@ class ImagePaint extends StatefulWidget { required this.keyboardEnabled, required this.remoteCursorMoved, required this.textureId, + required this.useTextureRender, this.listenerBuilder}) : super(key: key); @@ -485,15 +489,19 @@ class _ImagePaintState extends State { final imageWidth = c.getDisplayWidth() * s; final imageHeight = c.getDisplayHeight() * s; final imageSize = Size(imageWidth, imageHeight); - // final imageWidget = CustomPaint( - // size: imageSize, - // painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), - // ); - final imageWidget = SizedBox( - width: imageWidth, - height: imageHeight, - child: Obx(() => Texture(textureId: widget.textureId.value)), - ); + late final Widget imageWidget; + if (widget.useTextureRender) { + imageWidget = SizedBox( + width: imageWidth, + height: imageHeight, + child: Obx(() => Texture(textureId: widget.textureId.value)), + ); + } else { + imageWidget = CustomPaint( + size: imageSize, + painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + ); + } return NotificationListener( onNotification: (notification) { @@ -517,22 +525,27 @@ class _ImagePaintState extends State { context, _buildListener(imageWidget), c.size, imageSize)), )); } else { - // final imageWidget = CustomPaint( - // size: Size(c.size.width, c.size.height), - // painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), - // ); + late final Widget imageWidget; if (c.size.width > 0 && c.size.height > 0) { - final imageWidget = Stack( - children: [ - Positioned( - left: c.x, - top: c.y, - width: c.getDisplayWidth() * s, - height: c.getDisplayHeight() * s, - child: Texture(textureId: widget.textureId.value), - ) - ], - ); + if (widget.useTextureRender) { + imageWidget = Stack( + children: [ + Positioned( + left: c.x, + top: c.y, + width: c.getDisplayWidth() * s, + height: c.getDisplayHeight() * s, + child: Texture(textureId: widget.textureId.value), + ) + ], + ); + } else { + imageWidget = CustomPaint( + size: Size(c.size.width, c.size.height), + painter: + ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + ); + } return mouseRegion(child: _buildListener(imageWidget)); } else { return Container(); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a38db2a90..5ef72a0af 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1438,6 +1438,7 @@ class FFI { final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { + final useTextureRender = bind.mainUseTextureRender(); // Preserved for the rgba data. await for (final message in stream) { if (message is EventToUI_Event) { @@ -1451,17 +1452,7 @@ class FFI { debugPrint('json.decode fail1(): $e, ${message.field0}'); } } else if (message is EventToUI_Rgba) { - if (Platform.isAndroid || Platform.isIOS) { - // Fetch the image buffer from rust codes. - final sz = platformFFI.getRgbaSize(id); - if (sz == null || sz == 0) { - return; - } - final rgba = platformFFI.getRgba(id, sz); - if (rgba != null) { - imageModel.onRgba(rgba); - } - } else { + if (useTextureRender) { if (_waitForImage[id]!) { _waitForImage[id] = false; dialogManager.dismissAll(); @@ -1471,6 +1462,16 @@ class FFI { await canvasModel.updateViewStyle(); await canvasModel.updateScrollStyle(); } + } else { + // Fetch the image buffer from rust codes. + final sz = platformFFI.getRgbaSize(id); + if (sz == null || sz == 0) { + return; + } + final rgba = platformFFI.getRgba(id, sz); + if (rgba != null) { + imageModel.onRgba(rgba); + } } } } diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index acfd4c674..3adc24a14 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -11,7 +11,7 @@ use crate::hwcodec::*; use crate::mediacodec::{ MediaCodecDecoder, MediaCodecDecoders, H264_DECODER_SUPPORT, H265_DECODER_SUPPORT, }; -use crate::vpxcodec::*; +use crate::{vpxcodec::*, ImageFormat}; use hbb_common::{ anyhow::anyhow, @@ -306,16 +306,17 @@ impl Decoder { pub fn handle_video_frame( &mut self, frame: &video_frame::Union, + fmt: ImageFormat, rgb: &mut Vec, ) -> ResultType { match frame { video_frame::Union::Vp9s(vp9s) => { - Decoder::handle_vp9s_video_frame(&mut self.vpx, vp9s, rgb) + Decoder::handle_vp9s_video_frame(&mut self.vpx, vp9s, fmt, rgb) } #[cfg(feature = "hwcodec")] video_frame::Union::H264s(h264s) => { if let Some(decoder) = &mut self.hw.h264 { - Decoder::handle_hw_video_frame(decoder, h264s, rgb, &mut self.i420) + Decoder::handle_hw_video_frame(decoder, h264s, fmt, rgb, &mut self.i420) } else { Err(anyhow!("don't support h264!")) } @@ -323,7 +324,7 @@ impl Decoder { #[cfg(feature = "hwcodec")] video_frame::Union::H265s(h265s) => { if let Some(decoder) = &mut self.hw.h265 { - Decoder::handle_hw_video_frame(decoder, h265s, rgb, &mut self.i420) + Decoder::handle_hw_video_frame(decoder, h265s, fmt, rgb, &mut self.i420) } else { Err(anyhow!("don't support h265!")) } @@ -331,7 +332,7 @@ impl Decoder { #[cfg(feature = "mediacodec")] video_frame::Union::H264s(h264s) => { if let Some(decoder) = &mut self.media_codec.h264 { - Decoder::handle_mediacodec_video_frame(decoder, h264s, rgb) + Decoder::handle_mediacodec_video_frame(decoder, h264s, fmt, rgb) } else { Err(anyhow!("don't support h264!")) } @@ -339,7 +340,7 @@ impl Decoder { #[cfg(feature = "mediacodec")] video_frame::Union::H265s(h265s) => { if let Some(decoder) = &mut self.media_codec.h265 { - Decoder::handle_mediacodec_video_frame(decoder, h265s, rgb) + Decoder::handle_mediacodec_video_frame(decoder, h265s, fmt, rgb) } else { Err(anyhow!("don't support h265!")) } @@ -351,6 +352,7 @@ impl Decoder { fn handle_vp9s_video_frame( decoder: &mut VpxDecoder, vp9s: &EncodedVideoFrames, + fmt: ImageFormat, rgb: &mut Vec, ) -> ResultType { let mut last_frame = Image::new(); @@ -367,7 +369,7 @@ impl Decoder { if last_frame.is_null() { Ok(false) } else { - last_frame.rgb(1, true, rgb); + last_frame.to(fmt, 1, rgb); Ok(true) } } @@ -376,14 +378,15 @@ impl Decoder { fn handle_hw_video_frame( decoder: &mut HwDecoder, frames: &EncodedVideoFrames, - rgb: &mut Vec, + fmt: ImageFormat, + raw: &mut Vec, i420: &mut Vec, ) -> ResultType { let mut ret = false; for h264 in frames.frames.iter() { for image in decoder.decode(&h264.data)? { // TODO: just process the last frame - if image.bgra(rgb, i420).is_ok() { + if image.to_fmt(fmt, raw, i420).is_ok() { ret = true; } } @@ -395,11 +398,12 @@ impl Decoder { fn handle_mediacodec_video_frame( decoder: &mut MediaCodecDecoder, frames: &EncodedVideoFrames, - rgb: &mut Vec, + fmt: ImageFormat, + raw: &mut Vec, ) -> ResultType { let mut ret = false; for h264 in frames.frames.iter() { - return decoder.decode(&h264.data, rgb); + return decoder.decode(&h264.data, fmt, raw); } return Ok(false); } diff --git a/libs/scrap/src/common/convert.rs b/libs/scrap/src/common/convert.rs index 2b0223a0a..a2177805e 100644 --- a/libs/scrap/src/common/convert.rs +++ b/libs/scrap/src/common/convert.rs @@ -103,6 +103,19 @@ extern "C" { height: c_int, ) -> c_int; + pub fn I420ToABGR( + src_y: *const u8, + src_stride_y: c_int, + src_u: *const u8, + src_stride_u: c_int, + src_v: *const u8, + src_stride_v: c_int, + dst_rgba: *mut u8, + dst_stride_rgba: c_int, + width: c_int, + height: c_int, + ) -> c_int; + pub fn NV12ToARGB( src_y: *const u8, src_stride_y: c_int, @@ -246,6 +259,7 @@ pub unsafe fn nv12_to_i420( #[cfg(feature = "hwcodec")] pub mod hw { use hbb_common::{anyhow::anyhow, ResultType}; + use crate::ImageFormat; #[cfg(target_os = "windows")] use hwcodec::{ffmpeg::ffmpeg_linesize_offset_length, AVPixelFormat}; @@ -315,7 +329,8 @@ pub mod hw { } #[cfg(target_os = "windows")] - pub fn hw_nv12_to_bgra( + pub fn hw_nv12_to( + fmt: ImageFormat, width: usize, height: usize, src_y: &[u8], @@ -355,18 +370,39 @@ pub mod hw { width as _, height as _, ); - super::I420ToARGB( - i420_offset_y, - i420_stride_y, - i420_offset_u, - i420_stride_u, - i420_offset_v, - i420_stride_v, - dst.as_mut_ptr(), - (width * 4) as _, - width as _, - height as _, - ); + match fmt { + ImageFormat::ARGB => { + super::I420ToARGB( + i420_offset_y, + i420_stride_y, + i420_offset_u, + i420_stride_u, + i420_offset_v, + i420_stride_v, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ); + } + ImageFormat::ABGR => { + super::I420ToABGR( + i420_offset_y, + i420_stride_y, + i420_offset_u, + i420_stride_u, + i420_offset_v, + i420_stride_v, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ); + } + _ => { + return Err(anyhow!("unsupported image format")); + } + } return Ok(()); }; } @@ -374,7 +410,8 @@ pub mod hw { } #[cfg(not(target_os = "windows"))] - pub fn hw_nv12_to_bgra( + pub fn hw_nv12_to( + fmt: ImageFormat, width: usize, height: usize, src_y: &[u8], @@ -387,23 +424,46 @@ pub mod hw { ) -> ResultType<()> { dst.resize(width * height * 4, 0); unsafe { - match super::NV12ToARGB( - src_y.as_ptr(), - src_stride_y as _, - src_uv.as_ptr(), - src_stride_uv as _, - dst.as_mut_ptr(), - (width * 4) as _, - width as _, - height as _, - ) { - 0 => Ok(()), - _ => Err(anyhow!("NV12ToARGB failed")), + match fmt { + ImageFormat::ARGB => { + match super::NV12ToARGB( + src_y.as_ptr(), + src_stride_y as _, + src_uv.as_ptr(), + src_stride_uv as _, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ) { + 0 => Ok(()), + _ => Err(anyhow!("NV12ToARGB failed")), + } + } + ImageFormat::ABGR => { + match super::NV12ToABGR( + src_y.as_ptr(), + src_stride_y as _, + src_uv.as_ptr(), + src_stride_uv as _, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ) { + 0 => Ok(()), + _ => Err(anyhow!("NV12ToABGR failed")), + } + } + _ => { + Err(anyhow!("unsupported image format")); + } } } } - pub fn hw_i420_to_bgra( + pub fn hw_i420_to( + fmt: ImageFormat, width: usize, height: usize, src_y: &[u8], @@ -419,18 +479,38 @@ pub mod hw { let src_v = src_v.as_ptr(); dst.resize(width * height * 4, 0); unsafe { - super::I420ToARGB( - src_y, - src_stride_y as _, - src_u, - src_stride_u as _, - src_v, - src_stride_v as _, - dst.as_mut_ptr(), - (width * 4) as _, - width as _, - height as _, - ); + match fmt { + ImageFormat::ARGB => { + super::I420ToARGB( + src_y, + src_stride_y as _, + src_u, + src_stride_u as _, + src_v, + src_stride_v as _, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ); + } + ImageFormat::ABGR => { + super::I420ToABGR( + src_y, + src_stride_y as _, + src_u, + src_stride_u as _, + src_v, + src_stride_v as _, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ); + } + _ => { + } + } }; } } diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 27b157b79..d2b9f414f 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -1,6 +1,6 @@ use crate::{ codec::{EncoderApi, EncoderCfg}, - hw, HW_STRIDE_ALIGN, + hw, ImageFormat, HW_STRIDE_ALIGN, }; use hbb_common::{ anyhow::{anyhow, Context}, @@ -236,22 +236,24 @@ pub struct HwDecoderImage<'a> { } impl HwDecoderImage<'_> { - pub fn bgra(&self, bgra: &mut Vec, i420: &mut Vec) -> ResultType<()> { + pub fn to_fmt(&self, fmt: ImageFormat, fmt_data: &mut Vec, i420: &mut Vec) -> ResultType<()> { let frame = self.frame; match frame.pixfmt { - AVPixelFormat::AV_PIX_FMT_NV12 => hw::hw_nv12_to_bgra( + AVPixelFormat::AV_PIX_FMT_NV12 => hw::hw_nv12_to( + fmt, frame.width as _, frame.height as _, &frame.data[0], &frame.data[1], frame.linesize[0] as _, frame.linesize[1] as _, - bgra, + fmt_data, i420, HW_STRIDE_ALIGN, ), AVPixelFormat::AV_PIX_FMT_YUV420P => { - hw::hw_i420_to_bgra( + hw::hw_i420_to( + fmt, frame.width as _, frame.height as _, &frame.data[0], @@ -260,12 +262,20 @@ impl HwDecoderImage<'_> { frame.linesize[0] as _, frame.linesize[1] as _, frame.linesize[2] as _, - bgra, + fmt_data, ); return Ok(()); } } } + + pub fn bgra(&self, bgra: &mut Vec, i420: &mut Vec) -> ResultType<()> { + self.to_fmt(ImageFormat::ARGB, bgra, i420) + } + + pub fn rgba(&self, rgba: &mut Vec, i420: &mut Vec) -> ResultType<()> { + self.to_fmt(ImageFormat::ABGR, rgba, i420) + } } fn get_config(k: &str) -> ResultType { diff --git a/libs/scrap/src/common/mediacodec.rs b/libs/scrap/src/common/mediacodec.rs index 406baecb5..77c21ffcf 100644 --- a/libs/scrap/src/common/mediacodec.rs +++ b/libs/scrap/src/common/mediacodec.rs @@ -8,9 +8,10 @@ use std::{ time::Duration, }; +use crate::ImageFormat; use crate::{ codec::{EncoderApi, EncoderCfg}, - I420ToARGB, + I420ToABGR, I420ToARGB, }; /// MediaCodec mime type name @@ -50,7 +51,7 @@ impl MediaCodecDecoder { MediaCodecDecoders { h264, h265 } } - pub fn decode(&mut self, data: &[u8], rgb: &mut Vec) -> ResultType { + pub fn decode(&mut self, data: &[u8], fmt: ImageFormat, raw: &mut Vec) -> ResultType { match self.dequeue_input_buffer(Duration::from_millis(10))? { Some(mut input_buffer) => { let mut buf = input_buffer.buffer_mut(); @@ -83,23 +84,44 @@ impl MediaCodecDecoder { let bps = 4; let u = buf.len() * 2 / 3; let v = buf.len() * 5 / 6; - rgb.resize(h * w * bps, 0); + raw.resize(h * w * bps, 0); let y_ptr = buf.as_ptr(); let u_ptr = buf[u..].as_ptr(); let v_ptr = buf[v..].as_ptr(); unsafe { - I420ToARGB( - y_ptr, - stride, - u_ptr, - stride / 2, - v_ptr, - stride / 2, - rgb.as_mut_ptr(), - (w * bps) as _, - w as _, - h as _, - ); + match fmt { + ImageFormat::ARGB => { + I420ToARGB( + y_ptr, + stride, + u_ptr, + stride / 2, + v_ptr, + stride / 2, + raw.as_mut_ptr(), + (w * bps) as _, + w as _, + h as _, + ); + } + ImageFormat::ARGB => { + I420ToABGR( + y_ptr, + stride, + u_ptr, + stride / 2, + v_ptr, + stride / 2, + raw.as_mut_ptr(), + (w * bps) as _, + w as _, + h as _, + ); + } + _ => { + bail!("Unsupported image format"); + } + } } self.release_output_buffer(output_buffer, false)?; Ok(true) diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 45aafe7c5..c7da57734 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -43,6 +43,13 @@ pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer pub mod record; mod vpx; +#[derive(Copy, Clone)] +pub enum ImageFormat { + Raw, + ABGR, + ARGB, +} + #[inline] pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> { // does this really help? diff --git a/libs/scrap/src/common/vpxcodec.rs b/libs/scrap/src/common/vpxcodec.rs index 5164886a1..7a65b193d 100644 --- a/libs/scrap/src/common/vpxcodec.rs +++ b/libs/scrap/src/common/vpxcodec.rs @@ -6,8 +6,8 @@ use hbb_common::anyhow::{anyhow, Context}; use hbb_common::message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame}; use hbb_common::{get_time, ResultType}; -use crate::codec::EncoderApi; use crate::STRIDE_ALIGN; +use crate::{codec::EncoderApi, ImageFormat}; use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *}; use hbb_common::bytes::Bytes; @@ -417,7 +417,7 @@ impl VpxDecoder { Ok(Self { ctx }) } - pub fn decode2rgb(&mut self, data: &[u8], rgba: bool) -> Result> { + pub fn decode2rgb(&mut self, data: &[u8], fmt: ImageFormat) -> Result> { let mut img = Image::new(); for frame in self.decode(data)? { drop(img); @@ -431,7 +431,7 @@ impl VpxDecoder { Ok(Vec::new()) } else { let mut out = Default::default(); - img.rgb(1, rgba, &mut out); + img.to(fmt, 1, &mut out); Ok(out) } } @@ -539,40 +539,60 @@ impl Image { self.inner().stride[iplane] } - pub fn rgb(&self, stride_align: usize, rgba: bool, dst: &mut Vec) { + pub fn to(&self, fmt: ImageFormat, stride_align: usize, dst: &mut Vec) { let h = self.height(); let mut w = self.width(); - let bps = if rgba { 4 } else { 3 }; + let bps = match fmt { + ImageFormat::Raw => 3, + ImageFormat::ARGB | ImageFormat::ABGR => 4, + }; w = (w + stride_align - 1) & !(stride_align - 1); dst.resize(h * w * bps, 0); let img = self.inner(); unsafe { - if rgba { - super::I420ToARGB( - img.planes[0], - img.stride[0], - img.planes[1], - img.stride[1], - img.planes[2], - img.stride[2], - dst.as_mut_ptr(), - (w * bps) as _, - self.width() as _, - self.height() as _, - ); - } else { - super::I420ToRAW( - img.planes[0], - img.stride[0], - img.planes[1], - img.stride[1], - img.planes[2], - img.stride[2], - dst.as_mut_ptr(), - (w * bps) as _, - self.width() as _, - self.height() as _, - ); + match fmt { + ImageFormat::Raw => { + super::I420ToRAW( + img.planes[0], + img.stride[0], + img.planes[1], + img.stride[1], + img.planes[2], + img.stride[2], + dst.as_mut_ptr(), + (w * bps) as _, + self.width() as _, + self.height() as _, + ); + } + ImageFormat::ARGB => { + super::I420ToARGB( + img.planes[0], + img.stride[0], + img.planes[1], + img.stride[1], + img.planes[2], + img.stride[2], + dst.as_mut_ptr(), + (w * bps) as _, + self.width() as _, + self.height() as _, + ); + } + ImageFormat::ABGR => { + super::I420ToABGR( + img.planes[0], + img.stride[0], + img.planes[1], + img.stride[1], + img.planes[2], + img.stride[2], + dst.as_mut_ptr(), + (w * bps) as _, + self.width() as _, + self.height() as _, + ); + } } } } diff --git a/src/client.rs b/src/client.rs index aa3523185..9f4cef831 100644 --- a/src/client.rs +++ b/src/client.rs @@ -45,6 +45,7 @@ use scrap::{ codec::{Decoder, DecoderCfg}, record::{Recorder, RecorderContext}, VpxDecoderConfig, VpxVideoCodecId, + ImageFormat, }; use crate::{ @@ -943,7 +944,11 @@ impl VideoHandler { } match &vf.union { Some(frame) => { - let res = self.decoder.handle_video_frame(frame, &mut self.rgb); + #[cfg(feature = "flutter_texture_render")] + let fmt = ImageFormat::ARGB; + #[cfg(not(feature = "flutter_texture_render"))] + let fmt = ImageFormat::ABGR; + let res = self.decoder.handle_video_frame(frame, fmt, &mut self.rgb); if self.record { self.recorder .lock() diff --git a/src/flutter.rs b/src/flutter.rs index a5689bce6..f78e1bd92 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -128,19 +128,21 @@ pub struct FlutterHandler { pub event_stream: Arc>>>, // SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`. // We must check the `rgba_valid` before reading [rgba]. - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(not(feature = "flutter_texture_render"))] pub rgba: Arc>>, - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(not(feature = "flutter_texture_render"))] pub rgba_valid: Arc, - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(feature = "flutter_texture_render")] notify_rendered: Arc>, renderer: Arc>, peer_info: Arc>, } +#[cfg(feature = "flutter_texture_render")] pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(texture_rgba: *mut c_void, buffer: *const u8, width: c_int, height: c_int); // Video Texture Renderer in Flutter +#[cfg(feature = "flutter_texture_render")] #[derive(Clone)] struct VideoRenderer { // TextureRgba pointer in flutter native. @@ -151,6 +153,7 @@ struct VideoRenderer { on_rgba_func: Symbol<'static, FlutterRgbaRendererPluginOnRgba>, } +#[cfg(feature = "flutter_texture_render")] impl Default for VideoRenderer { fn default() -> Self { unsafe { @@ -167,6 +170,7 @@ impl Default for VideoRenderer { } } +#[cfg(feature = "flutter_texture_render")] impl VideoRenderer { #[inline] pub fn set_size(&mut self, width: i32, height: i32) { @@ -236,11 +240,13 @@ impl FlutterHandler { } #[inline] + #[cfg(feature = "flutter_texture_render")] pub fn register_texture(&mut self, ptr: usize) { self.renderer.write().unwrap().ptr = ptr; } #[inline] + #[cfg(feature = "flutter_texture_render")] pub fn set_size(&mut self, width: i32, height: i32) { *self.notify_rendered.write().unwrap() = false; self.renderer.write().unwrap().set_size(width, height); @@ -405,7 +411,7 @@ impl InvokeUiSession for FlutterHandler { fn adapt_size(&self) {} #[inline] - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(not(feature = "flutter_texture_render"))] fn on_rgba(&self, data: &mut Vec) { // If the current rgba is not fetched by flutter, i.e., is valid. // We give up sending a new event to flutter. @@ -421,7 +427,7 @@ impl InvokeUiSession for FlutterHandler { } #[inline] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(feature = "flutter_texture_render")] fn on_rgba(&self, data: &mut Vec) { self.renderer.read().unwrap().on_rgba(data); if *self.notify_rendered.read().unwrap() { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index c55866dbe..d8861eb0a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1306,6 +1306,17 @@ pub fn main_hide_docker() -> SyncReturn { SyncReturn(true) } +pub fn main_use_texture_render() -> SyncReturn { + #[cfg(not(feature = "flutter_texture_render"))] + { + SyncReturn(false) + } + #[cfg(feature = "flutter_texture_render")] + { + SyncReturn(true) + } +} + pub fn cm_start_listen_ipc_thread() { #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::flutter::connection_manager::start_listen_ipc_thread(); From 173e3bcd0d9d3a05ee8081cd52983906b8ad9d3f Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 22 Feb 2023 09:43:57 +0800 Subject: [PATCH 167/202] debug win, without hwcodec Signed-off-by: fufesou --- Cargo.toml | 1 + libs/scrap/src/common/mediacodec.rs | 3 +- src/client.rs | 4 +-- src/flutter.rs | 45 ++++++++++++++++++++--------- src/flutter_ffi.rs | 7 +++-- 5 files changed, 39 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0a7af0cbc..050a0cd47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ inline = [] hbbs = [] cli = [] with_rc = ["simple_rc"] +flutter_texture_render = [] appimage = [] flatpak = [] use_samplerate = ["samplerate"] diff --git a/libs/scrap/src/common/mediacodec.rs b/libs/scrap/src/common/mediacodec.rs index 77c21ffcf..7bda0b69d 100644 --- a/libs/scrap/src/common/mediacodec.rs +++ b/libs/scrap/src/common/mediacodec.rs @@ -1,5 +1,4 @@ -use hbb_common::anyhow::Error; -use hbb_common::{bail, ResultType}; +use hbb_common::{log, anyhow::Error, bail, ResultType}; use ndk::media::media_codec::{MediaCodec, MediaCodecDirection, MediaFormat}; use std::ops::Deref; use std::{ diff --git a/src/client.rs b/src/client.rs index 9f4cef831..0c2cf09cd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -945,9 +945,9 @@ impl VideoHandler { match &vf.union { Some(frame) => { #[cfg(feature = "flutter_texture_render")] - let fmt = ImageFormat::ARGB; - #[cfg(not(feature = "flutter_texture_render"))] let fmt = ImageFormat::ABGR; + #[cfg(not(feature = "flutter_texture_render"))] + let fmt = ImageFormat::ARGB; let res = self.decoder.handle_video_frame(frame, fmt, &mut self.rgb); if self.record { self.recorder diff --git a/src/flutter.rs b/src/flutter.rs index f78e1bd92..c232d891d 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -4,14 +4,17 @@ use crate::{ ui_session_interface::{io_loop, InvokeUiSession, Session}, }; use flutter_rust_bridge::StreamSink; +#[cfg(feature = "flutter_texture_render")] +use hbb_common::libc::c_void; use hbb_common::{ - bail, config::LocalConfig, get_version_number, libc::c_void, message_proto::*, - rendezvous_proto::ConnType, ResultType, + bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, + ResultType, }; +#[cfg(feature = "flutter_texture_render")] use libloading::{Library, Symbol}; use serde_json::json; -#[cfg(any(target_os = "android", target_os = "ios"))] +#[cfg(not(feature = "flutter_texture_render"))] use std::sync::atomic::{AtomicBool, Ordering}; use std::{ collections::HashMap, @@ -30,7 +33,10 @@ lazy_static::lazy_static! { pub static ref CUR_SESSION_ID: RwLock = Default::default(); pub static ref SESSIONS: RwLock>> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel - #[cfg(not(any(target_os = "ios", target_os = "android")))] +} + +#[cfg(feature = "flutter_texture_render")] +lazy_static::lazy_static! { pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Library = { unsafe { #[cfg(target_os = "windows")] @@ -134,6 +140,7 @@ pub struct FlutterHandler { pub rgba_valid: Arc, #[cfg(feature = "flutter_texture_render")] notify_rendered: Arc>, + #[cfg(feature = "flutter_texture_render")] renderer: Arc>, peer_info: Arc>, } @@ -556,7 +563,7 @@ impl InvokeUiSession for FlutterHandler { #[inline] fn get_rgba(&self) -> *const u8 { - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(not(feature = "flutter_texture_render"))] if self.rgba_valid.load(Ordering::Relaxed) { return self.rgba.read().unwrap().as_ptr(); } @@ -565,7 +572,7 @@ impl InvokeUiSession for FlutterHandler { #[inline] fn next_rgba(&self) { - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(not(feature = "flutter_texture_render"))] self.rgba_valid.store(false, Ordering::Relaxed); } } @@ -825,23 +832,28 @@ pub fn set_cur_session_id(id: String) { } #[no_mangle] -pub fn session_get_rgba_size(_id: *const char) -> usize { - #[cfg(any(target_os = "android", target_os = "ios"))] - let id = unsafe { std::ffi::CStr::from_ptr(_id as _) }; - #[cfg(any(target_os = "android", target_os = "ios"))] +#[cfg(not(feature = "flutter_texture_render"))] +pub fn session_get_rgba_size(id: *const char) -> usize { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; if let Ok(id) = id.to_str() { - if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + if let Some(session) = SESSIONS.read().unwrap().get(id) { return session.rgba.read().unwrap().len(); } } 0 } +#[no_mangle] +#[cfg(feature = "flutter_texture_render")] +pub fn session_get_rgba_size(_id: *const char) -> usize { + 0 +} + #[no_mangle] pub fn session_get_rgba(id: *const char) -> *const u8 { let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; if let Ok(id) = id.to_str() { - if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + if let Some(session) = SESSIONS.read().unwrap().get(id) { return session.get_rgba(); } } @@ -852,18 +864,23 @@ pub fn session_get_rgba(id: *const char) -> *const u8 { pub fn session_next_rgba(id: *const char) { let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; if let Ok(id) = id.to_str() { - if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + if let Some(session) = SESSIONS.read().unwrap().get(id) { return session.next_rgba(); } } } #[no_mangle] +#[cfg(feature = "flutter_texture_render")] pub fn session_register_texture(id: *const char, ptr: usize) { let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; if let Ok(id) = id.to_str() { - if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + if let Some(session) = SESSIONS.read().unwrap().get(id) { return session.register_texture(ptr); } } } + +#[no_mangle] +#[cfg(not(feature = "flutter_texture_render"))] +pub fn session_register_texture(_id: *const char, _ptr: usize) {} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d8861eb0a..14906d568 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -529,9 +529,10 @@ pub fn session_switch_sides(id: String) { } } -pub fn session_set_size(id: String, width: i32, height: i32) { - if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { - session.set_size(width, height); +pub fn session_set_size(_id: String, _width: i32, _height: i32) { + #[cfg(feature = "flutter_texture_render")] + if let Some(session) = SESSIONS.write().unwrap().get_mut(&_id) { + session.set_size(_width, _height); } } From d70ffaa2b86b0321d65f1a419d286e1b99b00c80 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 22 Feb 2023 09:57:51 +0800 Subject: [PATCH 168/202] update pubspec Signed-off-by: fufesou --- flutter/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index a07df9c2e..5ffe805b8 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1563,5 +1563,5 @@ packages: source: hosted version: "0.1.1" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.18.0 <4.0.0" flutter: ">=3.3.0" From ed0338b038b9ed087cae015f7db169295e30beff Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 22 Feb 2023 10:25:21 +0800 Subject: [PATCH 169/202] fix build && default flutter_texture_render Signed-off-by: fufesou --- build.py | 1 + libs/scrap/src/common/convert.rs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/build.py b/build.py index 9e490166f..727b53fe0 100755 --- a/build.py +++ b/build.py @@ -239,6 +239,7 @@ def get_features(args): features.append('hwcodec') if args.flutter: features.append('flutter') + features.append('flutter_texture_render') if args.flatpak: features.append('flatpak') if args.appimage: diff --git a/libs/scrap/src/common/convert.rs b/libs/scrap/src/common/convert.rs index a2177805e..f3ad51a21 100644 --- a/libs/scrap/src/common/convert.rs +++ b/libs/scrap/src/common/convert.rs @@ -126,6 +126,17 @@ extern "C" { width: c_int, height: c_int, ) -> c_int; + + pub fn NV12ToABGR( + src_y: *const u8, + src_stride_y: c_int, + src_uv: *const u8, + src_stride_uv: c_int, + dst_rgba: *mut u8, + dst_stride_rgba: c_int, + width: c_int, + height: c_int, + ) -> c_int; } // https://github.com/webmproject/libvpx/blob/master/vpx/src/vpx_image.c @@ -456,7 +467,7 @@ pub mod hw { } } _ => { - Err(anyhow!("unsupported image format")); + Err(anyhow!("unsupported image format")) } } } From 20021c6541b04caf1c414c7e4795ba6198349a1f Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 22 Feb 2023 11:03:40 +0800 Subject: [PATCH 170/202] fix build Signed-off-by: fufesou --- src/flutter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flutter.rs b/src/flutter.rs index c232d891d..42da3f038 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -875,7 +875,7 @@ pub fn session_next_rgba(id: *const char) { pub fn session_register_texture(id: *const char, ptr: usize) { let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; if let Ok(id) = id.to_str() { - if let Some(session) = SESSIONS.read().unwrap().get(id) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { return session.register_texture(ptr); } } From 8b7be688c27383c83962b064d843fbaa25e22eea Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 22 Feb 2023 22:33:17 +0800 Subject: [PATCH 171/202] macos, linux, r and b are reversed Signed-off-by: fufesou --- Cargo.lock | 327 +++++++++++-------- Cargo.toml | 1 + flutter/macos/Podfile.lock | 6 + flutter/macos/Runner/MainFlutterWindow.swift | 2 + src/flutter.rs | 82 +++-- 5 files changed, 256 insertions(+), 162 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb5461a6e..14b09a9d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -254,9 +254,9 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -271,9 +271,9 @@ version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -377,8 +377,8 @@ dependencies = [ "lazycell", "log", "peeking_take_while", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "regex", "rustc-hash", "shlex", @@ -397,12 +397,12 @@ dependencies = [ "lazy_static", "lazycell", "peeking_take_while", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "regex", "rustc-hash", "shlex", - "syn", + "syn 1.0.105", ] [[package]] @@ -588,11 +588,11 @@ dependencies = [ "heck 0.4.0", "indexmap", "log", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "serde 1.0.149", "serde_json 1.0.89", - "syn", + "syn 1.0.105", "tempfile", "toml", ] @@ -721,9 +721,9 @@ checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck 0.4.0", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1119,10 +1119,10 @@ dependencies = [ "cc", "codespan-reporting", "once_cell", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "scratch", - "syn", + "syn 1.0.105", ] [[package]] @@ -1137,9 +1137,9 @@ version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1177,10 +1177,10 @@ checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ "fnv", "ident_case", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "strsim 0.10.0", - "syn", + "syn 1.0.105", ] [[package]] @@ -1190,8 +1190,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1373,9 +1373,9 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "082a24a9967533dc5d743c602157637116fc1b52806d694a5a45e6f32567fcdd" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1384,9 +1384,9 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1481,6 +1481,29 @@ dependencies = [ "libloading", ] +[[package]] +name = "dlopen" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e80ad39f814a9abe68583cd50a2d45c8a67561c3361ab8da240587dda80937" +dependencies = [ + "dlopen_derive", + "lazy_static", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "dlopen_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f236d9e1b1fbd81cea0f9cbdc8dcc7e8ebcd80e6659cd7cb2ad5f6c05946c581" +dependencies = [ + "libc", + "quote 0.6.13", + "syn 0.15.44", +] + [[package]] name = "dlv-list" version = "0.3.0" @@ -1592,9 +1615,9 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9045e2676cd5af83c3b167d917b0a5c90a4d8e266e2683d6631b235c457fc27" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1604,9 +1627,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eb359f1476bf611266ac1f5355bc14aeca37b299d0ebccc038ee7058891c9cb" dependencies = [ "once_cell", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1625,9 +1648,9 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1670,10 +1693,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34a887c8df3ed90498c1c437ce21f211c8e27672921a8ffa293cb8d6d4caa9e" dependencies = [ "proc-macro-error", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "rustversion", - "syn", + "syn 1.0.105", "synstructure", ] @@ -1747,9 +1770,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c5216e387a76eebaaf11f6d871ec8a4aae0b25f05456ee21f228e024b1b3610" dependencies = [ "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1878,11 +1901,11 @@ dependencies = [ "lazy_static", "log", "pathdiff", - "quote", + "quote 1.0.21", "regex", "serde 1.0.149", "serde_yaml", - "syn", + "syn 1.0.105", "tempfile", "thiserror", "toml", @@ -2035,9 +2058,9 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -2299,9 +2322,9 @@ dependencies = [ "itertools 0.9.0", "proc-macro-crate 0.1.5", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -2314,9 +2337,9 @@ dependencies = [ "heck 0.4.0", "proc-macro-crate 1.2.1", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -2550,9 +2573,9 @@ dependencies = [ "anyhow", "proc-macro-crate 1.2.1", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -2856,8 +2879,8 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", ] [[package]] @@ -3564,9 +3587,9 @@ checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ "darling", "proc-macro-crate 1.2.1", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -3713,9 +3736,9 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -3805,9 +3828,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ "proc-macro-crate 1.2.1", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -4121,9 +4144,9 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -4230,9 +4253,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", "version_check", ] @@ -4242,11 +4265,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "version_check", ] +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + [[package]] name = "proc-macro2" version = "1.0.47" @@ -4374,13 +4406,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + [[package]] name = "quote" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ - "proc-macro2", + "proc-macro2 1.0.47", ] [[package]] @@ -4846,6 +4887,7 @@ dependencies = [ "dbus-crossroads", "default-net", "dispatch", + "dlopen", "enigo", "errno", "evdev", @@ -5158,9 +5200,9 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -5192,9 +5234,9 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -5453,9 +5495,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" dependencies = [ "heck 0.3.3", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -5465,10 +5507,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck 0.4.0", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "rustversion", - "syn", + "syn 1.0.105", +] + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", ] [[package]] @@ -5477,8 +5530,8 @@ version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "unicode-ident", ] @@ -5488,10 +5541,10 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", + "unicode-xid 0.2.4", ] [[package]] @@ -5630,9 +5683,9 @@ name = "tao-macros" version = "0.1.0" source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -5725,9 +5778,9 @@ version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -5831,9 +5884,9 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -5920,9 +5973,9 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -6033,6 +6086,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + [[package]] name = "unicode-xid" version = "0.2.4" @@ -6177,9 +6236,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", "wasm-bindgen-shared", ] @@ -6201,7 +6260,7 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ - "quote", + "quote 1.0.21", "wasm-bindgen-macro-support", ] @@ -6211,9 +6270,9 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6281,8 +6340,8 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "xml-rs", ] @@ -6507,9 +6566,9 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -6518,9 +6577,9 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -6962,10 +7021,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e" dependencies = [ "proc-macro-crate 1.2.1", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "regex", - "syn", + "syn 1.0.105", ] [[package]] @@ -7038,7 +7097,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" dependencies = [ "proc-macro-crate 1.2.1", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] diff --git a/Cargo.toml b/Cargo.toml index 050a0cd47..b51930f72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" rdev = { git = "https://github.com/fufesou/rdev" } url = { version = "2.1", features = ["serde"] } +dlopen = "0.1" reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } chrono = "0.4.23" diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index 3187c6349..16dc0d352 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -21,6 +21,8 @@ PODS: - sqflite (0.0.2): - FlutterMacOS - FMDB (>= 2.7.5) + - texture_rgba_renderer (0.0.1): + - FlutterMacOS - uni_links_desktop (0.0.1): - FlutterMacOS - url_launcher_macos (0.0.1): @@ -42,6 +44,7 @@ DEPENDENCIES: - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - texture_rgba_renderer (from `Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos`) - uni_links_desktop (from `Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) @@ -71,6 +74,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + texture_rgba_renderer: + :path: Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos uni_links_desktop: :path: Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos url_launcher_macos: @@ -93,6 +98,7 @@ SPEC CHECKSUMS: path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2 uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 21e870320..e9043da71 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -17,6 +17,7 @@ import url_launcher_macos import wakelock_macos import window_manager import window_size +import texture_rgba_renderer class MainFlutterWindow: NSWindow { override func awakeFromNib() { @@ -49,6 +50,7 @@ class MainFlutterWindow: NSWindow { UrlLauncherPlugin.register(with: controller.registrar(forPlugin: "UrlLauncherPlugin")) WakelockMacosPlugin.register(with: controller.registrar(forPlugin: "WakelockMacosPlugin")) WindowSizePlugin.register(with: controller.registrar(forPlugin: "WindowSizePlugin")) + TextureRgbaRendererPlugin.register(with: controller.registrar(forPlugin: "TextureRgbaRendererPlugin")) } super.awakeFromNib() diff --git a/src/flutter.rs b/src/flutter.rs index 42da3f038..51c96ddcf 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -3,15 +3,22 @@ use crate::{ flutter_ffi::EventToUI, ui_session_interface::{io_loop, InvokeUiSession, Session}, }; +#[cfg(feature = "flutter_texture_render")] +#[cfg(target_os = "macos")] +use dlopen::{ + symbor::{Library, Symbol}, + Error as LibError, +}; use flutter_rust_bridge::StreamSink; #[cfg(feature = "flutter_texture_render")] use hbb_common::libc::c_void; use hbb_common::{ - bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, - ResultType, + bail, config::LocalConfig, get_version_number, log, message_proto::*, + rendezvous_proto::ConnType, ResultType, }; #[cfg(feature = "flutter_texture_render")] -use libloading::{Library, Symbol}; +#[cfg(not(target_os = "macos"))] +use libloading::{Error as LibError, Library, Symbol}; use serde_json::json; #[cfg(not(feature = "flutter_texture_render"))] @@ -37,16 +44,16 @@ lazy_static::lazy_static! { #[cfg(feature = "flutter_texture_render")] lazy_static::lazy_static! { - pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Library = { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = { + #[cfg(not(target_os = "macos"))] unsafe { #[cfg(target_os = "windows")] - let lib = Library::new("texture_rgba_renderer_plugin.dll"); - #[cfg(target_os = "macos")] - let lib = Library::new("texture_rgba_renderer_plugin.dylib"); + Library::new("texture_rgba_renderer_plugin.dll"); #[cfg(target_os = "linux")] - let lib = Library::new("libtexture_rgba_renderer_plugin.so"); - lib.expect("`libtexture_rgba_renderer_plugin` not found, please add `texture_rgba_renderer` in your flutter project") + Library::new("libtexture_rgba_renderer_plugin.so"); } + #[cfg(target_os = "macos")] + Library::open_self() }; } @@ -157,22 +164,40 @@ struct VideoRenderer { width: i32, height: i32, data_len: usize, - on_rgba_func: Symbol<'static, FlutterRgbaRendererPluginOnRgba>, + on_rgba_func: Option>, } #[cfg(feature = "flutter_texture_render")] impl Default for VideoRenderer { fn default() -> Self { - unsafe { - Self { - ptr: 0, - width: 0, - height: 0, - data_len: 0, - on_rgba_func: TEXTURE_RGBA_RENDERER_PLUGIN - .get::(b"FlutterRgbaRendererPluginOnRgba") - .expect("Symbol FlutterRgbaRendererPluginOnRgba not found."), + let on_rgba_func = match &*TEXTURE_RGBA_RENDERER_PLUGIN { + Ok(lib) => { + #[cfg(not(target_os = "macos"))] + let find_sym_res = + lib.get::(b"FlutterRgbaRendererPluginOnRgba"); + #[cfg(target_os = "macos")] + let find_sym_res = unsafe { + lib.symbol::("FlutterRgbaRendererPluginOnRgba") + }; + match find_sym_res { + Ok(sym) => Some(sym), + Err(e) => { + log::error!("Failed to find symbol FlutterRgbaRendererPluginOnRgba, {e}"); + None + } + } } + Err(e) => { + log::error!("Failed to load texture rgba renderer plugin, {e}"); + None + } + }; + Self { + ptr: 0, + width: 0, + height: 0, + data_len: 0, + on_rgba_func, } } } @@ -194,15 +219,16 @@ impl VideoRenderer { if self.ptr == usize::default() || rgba.len() != self.data_len { return; } - let func = self.on_rgba_func.clone(); - unsafe { - func( - self.ptr as _, - rgba.as_ptr() as _, - self.width as _, - self.height as _, - ) - }; + if let Some(func) = &self.on_rgba_func { + unsafe { + func( + self.ptr as _, + rgba.as_ptr() as _, + self.width as _, + self.height as _, + ) + }; + } } } From 9559a889fbefc53912b3a049d347344fb7723897 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 10:02:54 +0800 Subject: [PATCH 172/202] register plugin && fix r&b colors Signed-off-by: fufesou --- flutter/windows/runner/flutter_window.cpp | 10 ++++++++++ src/client.rs | 5 +++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/flutter/windows/runner/flutter_window.cpp b/flutter/windows/runner/flutter_window.cpp index b43b9095e..2f1f36f73 100644 --- a/flutter/windows/runner/flutter_window.cpp +++ b/flutter/windows/runner/flutter_window.cpp @@ -2,6 +2,9 @@ #include +#include +#include + #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) @@ -25,6 +28,13 @@ bool FlutterWindow::OnCreate() { return false; } RegisterPlugins(flutter_controller_->engine()); + DesktopMultiWindowSetWindowCreatedCallback([](void *controller) { + auto *flutter_view_controller = + reinterpret_cast(controller); + auto *registry = flutter_view_controller->engine(); + TextureRgbaRendererPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("TextureRgbaRendererPlugin")); + }); SetChildContent(flutter_controller_->view()->GetNativeWindow()); return true; } diff --git a/src/client.rs b/src/client.rs index 0c2cf09cd..ebfda7283 100644 --- a/src/client.rs +++ b/src/client.rs @@ -944,9 +944,10 @@ impl VideoHandler { } match &vf.union { Some(frame) => { - #[cfg(feature = "flutter_texture_render")] + // windows && flutter_texture_render, fmt is ImageFormat::ABGR + #[cfg(all(target_os = "windows", feature = "flutter_texture_render"))] let fmt = ImageFormat::ABGR; - #[cfg(not(feature = "flutter_texture_render"))] + #[cfg(not(all(target_os = "windows", feature = "flutter_texture_render")))] let fmt = ImageFormat::ARGB; let res = self.decoder.handle_video_frame(frame, fmt, &mut self.rgb); if self.record { From b8e381d79d30b7d47013ee9e0fd6bbefefcfb92d Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 10:21:31 +0800 Subject: [PATCH 173/202] win, debug Signed-off-by: fufesou --- Cargo.lock | 1 - Cargo.toml | 1 - src/flutter.rs | 56 ++++++++++++++++++++++++-------------------------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 14b09a9d2..8483cbac1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4902,7 +4902,6 @@ dependencies = [ "include_dir", "jni 0.19.0", "lazy_static", - "libloading", "libpulse-binding", "libpulse-simple-binding", "mac_address", diff --git a/Cargo.toml b/Cargo.toml index b51930f72..b424b01d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,6 @@ dlopen = "0.1" reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } chrono = "0.4.23" cidr-utils = "0.5.9" -libloading = "0.7.4" [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.13.5" diff --git a/src/flutter.rs b/src/flutter.rs index 51c96ddcf..f2f950ad3 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -4,21 +4,18 @@ use crate::{ ui_session_interface::{io_loop, InvokeUiSession, Session}, }; #[cfg(feature = "flutter_texture_render")] -#[cfg(target_os = "macos")] +// #[cfg(target_os = "macos")] use dlopen::{ symbor::{Library, Symbol}, Error as LibError, }; use flutter_rust_bridge::StreamSink; -#[cfg(feature = "flutter_texture_render")] -use hbb_common::libc::c_void; use hbb_common::{ - bail, config::LocalConfig, get_version_number, log, message_proto::*, - rendezvous_proto::ConnType, ResultType, + bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, + ResultType, }; #[cfg(feature = "flutter_texture_render")] -#[cfg(not(target_os = "macos"))] -use libloading::{Error as LibError, Library, Symbol}; +use hbb_common::{libc::c_void, log}; use serde_json::json; #[cfg(not(feature = "flutter_texture_render"))] @@ -42,19 +39,19 @@ lazy_static::lazy_static! { pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } -#[cfg(feature = "flutter_texture_render")] +#[cfg(all(target_os = "windows", feature = "flutter_texture_render"))] lazy_static::lazy_static! { - pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = { - #[cfg(not(target_os = "macos"))] - unsafe { - #[cfg(target_os = "windows")] - Library::new("texture_rgba_renderer_plugin.dll"); - #[cfg(target_os = "linux")] - Library::new("libtexture_rgba_renderer_plugin.so"); - } - #[cfg(target_os = "macos")] - Library::open_self() - }; + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open("texture_rgba_renderer_plugin.dll"); +} + +#[cfg(all(target_os = "linux", feature = "flutter_texture_render"))] +lazy_static::lazy_static! { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open("libtexture_rgba_renderer_plugin.so"); +} + +#[cfg(all(target_os = "macos", feature = "flutter_texture_render"))] +lazy_static::lazy_static! { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open_self(); } /// FFI for rustdesk core's main entry. @@ -136,21 +133,26 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { // Afterwards the vector will be dropped and thus freed. } +#[cfg(feature = "flutter_texture_render")] +#[derive(Default, Clone)] +pub struct FlutterHandler { + pub event_stream: Arc>>>, + notify_rendered: Arc>, + renderer: Arc>, + peer_info: Arc>, +} + +#[cfg(not(feature = "flutter_texture_render"))] #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, // SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`. // We must check the `rgba_valid` before reading [rgba]. - #[cfg(not(feature = "flutter_texture_render"))] pub rgba: Arc>>, - #[cfg(not(feature = "flutter_texture_render"))] pub rgba_valid: Arc, - #[cfg(feature = "flutter_texture_render")] - notify_rendered: Arc>, - #[cfg(feature = "flutter_texture_render")] - renderer: Arc>, peer_info: Arc>, } + #[cfg(feature = "flutter_texture_render")] pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(texture_rgba: *mut c_void, buffer: *const u8, width: c_int, height: c_int); @@ -172,10 +174,6 @@ impl Default for VideoRenderer { fn default() -> Self { let on_rgba_func = match &*TEXTURE_RGBA_RENDERER_PLUGIN { Ok(lib) => { - #[cfg(not(target_os = "macos"))] - let find_sym_res = - lib.get::(b"FlutterRgbaRendererPluginOnRgba"); - #[cfg(target_os = "macos")] let find_sym_res = unsafe { lib.symbol::("FlutterRgbaRendererPluginOnRgba") }; From b84062b8f414317879c31558c743ca07f435838d Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 13:40:08 +0800 Subject: [PATCH 174/202] texture render, add log info Signed-off-by: fufesou --- src/flutter.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index f2f950ad3..c501bd4a5 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -4,18 +4,17 @@ use crate::{ ui_session_interface::{io_loop, InvokeUiSession, Session}, }; #[cfg(feature = "flutter_texture_render")] -// #[cfg(target_os = "macos")] use dlopen::{ symbor::{Library, Symbol}, Error as LibError, }; use flutter_rust_bridge::StreamSink; -use hbb_common::{ - bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, - ResultType, -}; #[cfg(feature = "flutter_texture_render")] -use hbb_common::{libc::c_void, log}; +use hbb_common::libc::c_void; +use hbb_common::{ + bail, config::LocalConfig, get_version_number, log, message_proto::*, + rendezvous_proto::ConnType, ResultType, +}; use serde_json::json; #[cfg(not(feature = "flutter_texture_render"))] @@ -665,6 +664,13 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy *session.event_stream.write().unwrap() = Some(event_stream); let session = session.clone(); std::thread::spawn(move || { + #[cfg(feature = "flutter_texture_render")] + log::info!( + "Session {} start, render by flutter texture rgba plugin", + id + ); + #[cfg(not(feature = "flutter_texture_render"))] + log::info!("Session {} start, render by flutter paint widget", id); io_loop(session); }); Ok(()) From 09aa42c53344c6d100adf481fbceec0ae64babfe Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 14:02:16 +0800 Subject: [PATCH 175/202] fix build Signed-off-by: fufesou --- src/flutter.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index c501bd4a5..d366a0eda 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -661,16 +661,16 @@ pub fn session_add( /// * `events2ui` - The events channel to ui. pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultType<()> { if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + #[cfg(feature = "flutter_texture_render")] + log::info!( + "Session {} start, render by flutter texture rgba plugin", + id + ); + #[cfg(not(feature = "flutter_texture_render"))] + log::info!("Session {} start, render by flutter paint widget", id); *session.event_stream.write().unwrap() = Some(event_stream); let session = session.clone(); std::thread::spawn(move || { - #[cfg(feature = "flutter_texture_render")] - log::info!( - "Session {} start, render by flutter texture rgba plugin", - id - ); - #[cfg(not(feature = "flutter_texture_render"))] - log::info!("Session {} start, render by flutter paint widget", id); io_loop(session); }); Ok(()) From 4cb6e82893565a97f80ef97cc639c852a822391c Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 15:16:32 +0800 Subject: [PATCH 176/202] add feature flutter_texture_render for linux Signed-off-by: fufesou --- .github/workflows/flutter-nightly.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index ffcadd18b..b08193971 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -732,7 +732,7 @@ jobs: x86_64) # no need mock on x86_64 export VCPKG_ROOT=/opt/artifacts/vcpkg - cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features hwcodec,flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release ;; esac @@ -900,7 +900,7 @@ jobs: ln -s /usr/include /vcpkg/installed/arm64-linux/include export VCPKG_ROOT=/vcpkg # disable hwcodec for compilation - cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release ;; armv7) cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ @@ -910,7 +910,7 @@ jobs: ln -s /usr/include /vcpkg/installed/arm-linux/include export VCPKG_ROOT=/vcpkg # disable hwcodec for compilation - cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release ;; esac From 275da850ffaf5af6c57d313ce5480eee495753c2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 16:30:12 +0800 Subject: [PATCH 177/202] do not create texture when texture render is not enabled Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 24 +++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index c78ffb439..e52334512 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -126,14 +126,16 @@ class _RemotePageState extends State } // Register texture. _textureId.value = -1; - textureRenderer.createTexture(_textureKey).then((id) async { - debugPrint("id: $id, texture_key: $_textureKey"); - if (id != -1) { - final ptr = await textureRenderer.getTexturePtr(_textureKey); - platformFFI.registerTexture(widget.id, ptr); - _textureId.value = id; - } - }); + 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(widget.id, ptr); + _textureId.value = id; + } + }); + } _ffi.ffiModel.updateEventListener(widget.id); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // Session option should be set after models.dart/FFI.start @@ -198,8 +200,10 @@ class _RemotePageState extends State @override void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); - platformFFI.registerTexture(widget.id, 0); - textureRenderer.closeTexture(_textureKey); + if (useTextureRender) { + platformFFI.registerTexture(widget.id, 0); + textureRenderer.closeTexture(_textureKey); + } // ensure we leave this session, this is a double check bind.sessionEnterOrLeave(id: widget.id, enter: false); DesktopMultiWindow.removeListener(this); From aeed94bb96963be3018717f2b726504bdc04f74c Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 18:03:40 +0800 Subject: [PATCH 178/202] update flutter-ci && restore crate-type Signed-off-by: fufesou --- .github/workflows/flutter-ci.yml | 6 +++--- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 2386f17dd..74e4efa99 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -593,7 +593,7 @@ jobs: x86_64) # no need mock on x86_64 export VCPKG_ROOT=/opt/artifacts/vcpkg - cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features hwcodec,flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release ;; esac @@ -761,7 +761,7 @@ jobs: ln -s /usr/include /vcpkg/installed/arm64-linux/include export VCPKG_ROOT=/vcpkg # disable hwcodec for compilation - cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release ;; armv7) cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ @@ -771,7 +771,7 @@ jobs: ln -s /usr/include /vcpkg/installed/arm-linux/include export VCPKG_ROOT=/vcpkg # disable hwcodec for compilation - cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release ;; esac diff --git a/Cargo.toml b/Cargo.toml index b424b01d1..c20366983 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ default-run = "rustdesk" [lib] name = "librustdesk" -crate-type = ["cdylib"] +crate-type = ["cdylib", "staticlib", "rlib"] [[bin]] name = "naming" From 75fb964a340b85a39e36152a56c93b1865f48f1e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 23 Feb 2023 19:08:44 +0800 Subject: [PATCH 179/202] opt: lack of frame border in remote page --- .../desktop/pages/file_manager_tab_page.dart | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index bbe2b28be..148d928d9 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -86,14 +86,18 @@ class _FileManagerTabPageState extends State { @override Widget build(BuildContext context) { - final tabWidget = Scaffold( - backgroundColor: Theme.of(context).cardColor, - body: DesktopTab( - controller: tabController, - onWindowCloseButton: handleWindowCloseButton, - tail: const AddButton().paddingOnly(left: 10), - labelGetter: DesktopTab.labelGetterAlias, - )); + final tabWidget = Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: Theme.of(context).cardColor, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton().paddingOnly(left: 10), + labelGetter: DesktopTab.labelGetterAlias, + )), + ); return Platform.isMacOS ? tabWidget : SubWindowDragToResizeArea( From fdc04266f6f016efc39ef4dc02a9a342520db46a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 23 Feb 2023 19:28:30 +0800 Subject: [PATCH 180/202] fix #1947 --- src/tray.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tray.rs b/src/tray.rs index 12523605d..5e1620036 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -112,8 +112,8 @@ pub fn make_tray() -> hbb_common::ResultType<()> { const LIGHT: &[u8] = include_bytes!("../res/mac-tray-light-x2.png"); const DARK: &[u8] = include_bytes!("../res/mac-tray-dark-x2.png"); let icon = match mode { - dark_light::Mode::Dark => DARK, - _ => LIGHT, + dark_light::Mode::Dark => LIGHT, + _ => DARK, }; let (icon_rgba, icon_width, icon_height) = { let image = image::load_from_memory(icon) @@ -147,7 +147,7 @@ pub fn make_tray() -> hbb_common::ResultType<()> { crate::platform::macos::hide_dock(); docker_hiden = true; } - *control_flow = ControlFlow::Poll; + *control_flow = ControlFlow::Wait; if tray_channel.try_recv().is_ok() { crate::platform::macos::handle_application_should_open_untitled_file(); From bb26ba3384bde03c5a45c931a14d0cfcd4d5cea4 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 23 Feb 2023 20:01:50 +0800 Subject: [PATCH 181/202] Exit in mac tray --- src/platform/macos.rs | 25 ++++++++++++++++--------- src/tray.rs | 24 +++++++++++++++++++++--- src/ui_interface.rs | 2 +- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 910c26982..3e19cca28 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -171,7 +171,7 @@ pub fn is_installed_daemon(prompt: bool) -> bool { false } -pub fn uninstall() -> bool { +pub fn uninstall(show_new_window: bool) -> bool { // to-do: do together with win/linux about refactory start/stop service if !is_installed_daemon(false) { return false; @@ -206,14 +206,21 @@ pub fn uninstall() -> bool { .args(&["remove", &format!("{}_server", crate::get_full_name())]) .status() .ok(); - std::process::Command::new("sh") - .arg("-c") - .arg(&format!( - "sleep 0.5; open /Applications/{}.app", - crate::get_app_name(), - )) - .spawn() - .ok(); + if show_new_window { + std::process::Command::new("sh") + .arg("-c") + .arg(&format!( + "sleep 0.5; open /Applications/{}.app", + crate::get_app_name(), + )) + .spawn() + .ok(); + } else { + std::process::Command::new("pkill") + .arg(crate::get_app_name()) + .status() + .ok(); + } quit_gui(); } } diff --git a/src/tray.rs b/src/tray.rs index 5e1620036..617ec2c93 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -107,7 +107,10 @@ pub fn make_tray() -> hbb_common::ResultType<()> { // https://github.com/tauri-apps/tray-icon/blob/dev/examples/tao.rs use hbb_common::anyhow::Context; use tao::event_loop::{ControlFlow, EventLoopBuilder}; - use tray_icon::{TrayEvent, TrayIconBuilder}; + use tray_icon::{ + menu::{Menu, MenuEvent, MenuItem}, + ClickEvent, TrayEvent, TrayIconBuilder, + }; let mode = dark_light::detect(); const LIGHT: &[u8] = include_bytes!("../res/mac-tray-light-x2.png"); const DARK: &[u8] = include_bytes!("../res/mac-tray-dark-x2.png"); @@ -128,8 +131,13 @@ pub fn make_tray() -> hbb_common::ResultType<()> { let event_loop = EventLoopBuilder::new().build(); + let tray_menu = Menu::new(); + let quit_i = MenuItem::new(crate::client::translate("Exit".to_owned()), true, None); + tray_menu.append_items(&[&quit_i]); + let _tray_icon = Some( TrayIconBuilder::new() + .with_menu(Box::new(tray_menu)) .with_tooltip(format!( "{} {}", crate::get_app_name(), @@ -139,6 +147,7 @@ pub fn make_tray() -> hbb_common::ResultType<()> { .build()?, ); + let menu_channel = MenuEvent::receiver(); let tray_channel = TrayEvent::receiver(); let mut docker_hiden = false; @@ -149,8 +158,17 @@ pub fn make_tray() -> hbb_common::ResultType<()> { } *control_flow = ControlFlow::Wait; - if tray_channel.try_recv().is_ok() { - crate::platform::macos::handle_application_should_open_untitled_file(); + if let Ok(event) = menu_channel.try_recv() { + if event.id == quit_i.id() { + crate::platform::macos::uninstall(false); + } + println!("{event:?}"); + } + + if let Ok(event) = tray_channel.try_recv() { + if event.event == ClickEvent::Double { + crate::platform::macos::handle_application_should_open_untitled_file(); + } } }); } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index f44bb4eea..dd111f86e 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -295,7 +295,7 @@ pub fn set_option(key: String, value: String) { #[cfg(target_os = "macos")] if &key == "stop-service" { let is_stop = value == "Y"; - if is_stop && crate::platform::macos::uninstall() { + if is_stop && crate::platform::macos::uninstall(true) { return; } } From a149ba832b293a0601296da3c2fc62588db262cb Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 17 Feb 2023 20:07:21 +0100 Subject: [PATCH 182/202] PeerCard. Menu. Move "remove" to last position. --- flutter/lib/common/widgets/peer_card.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index f1b94ecdf..7b24ec2e4 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -690,9 +690,6 @@ class RecentPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id)); - menuItems.add(_removeAction(peer.id, () async { - await bind.mainLoadRecentPeers(); - })); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } @@ -700,6 +697,9 @@ class RecentPeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadRecentPeers(); + })); return menuItems; } @@ -732,9 +732,6 @@ class FavoritePeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id)); - menuItems.add(_removeAction(peer.id, () async { - await bind.mainLoadFavPeers(); - })); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } @@ -744,6 +741,9 @@ class FavoritePeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadFavPeers(); + })); return menuItems; } @@ -775,10 +775,10 @@ class DiscoveredPeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } menuItems.add(MenuEntryDivider()); - menuItems.add(_removeAction(peer.id, () async {})); if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(_removeAction(peer.id, () async {})); return menuItems; } @@ -811,13 +811,13 @@ class AddressBookPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id)); - menuItems.add(_removeAction(peer.id, () async {})); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } if (gFFI.abModel.tags.isNotEmpty) { menuItems.add(_editTagAction(peer.id)); } + menuItems.add(_removeAction(peer.id, () async {})); return menuItems; } From 02b5085e2b681e4e47075fd67a24d5873f479974 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 23 Feb 2023 13:07:59 +0100 Subject: [PATCH 183/202] PeerCard. Menu. Make "remove" more visible --- flutter/lib/common/widgets/peer_card.dart | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 7b24ec2e4..6ea6a97aa 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -515,9 +515,21 @@ abstract class BasePeerCard extends StatelessWidget { String id, Future Function() reloadFunc, {bool isLan = false}) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Remove'), - style: style, + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Remove'), + style: style?.copyWith(color: Colors.red), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.delete_forever, color: Colors.red), + ), + ).marginOnly(right: 4)), + ], ), proc: () { () async { @@ -697,6 +709,7 @@ class RecentPeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadRecentPeers(); })); @@ -741,6 +754,7 @@ class FavoritePeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadFavPeers(); })); @@ -778,6 +792,7 @@ class DiscoveredPeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); return menuItems; } @@ -817,6 +832,7 @@ class AddressBookPeerCard extends BasePeerCard { if (gFFI.abModel.tags.isNotEmpty) { menuItems.add(_editTagAction(peer.id)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); return menuItems; } From 6ae0456c45bb2f9419c35e6bb7dd4e2e8e5d0bec Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 23 Feb 2023 13:11:49 +0100 Subject: [PATCH 184/202] PeerCard. Menu. Change button text "remove" to "delete" --- flutter/lib/common/widgets/peer_card.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 6ea6a97aa..4a376c588 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -518,7 +518,7 @@ abstract class BasePeerCard extends StatelessWidget { childBuilder: (TextStyle? style) => Row( children: [ Text( - translate('Remove'), + translate('Delete'), style: style?.copyWith(color: Colors.red), ), Expanded( From b139c90dd793e3dd65533aec12ed445f3f12ee51 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Mon, 20 Feb 2023 20:25:37 +0100 Subject: [PATCH 185/202] PeerCard. Menu. Make "add to favorites" dynamic --- flutter/lib/common/widgets/peer_card.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 4a376c588..9d9d3d01f 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -689,6 +689,9 @@ class RecentPeerCard extends BasePeerCard { _connectAction(context, peer), _transferFileAction(context, peer.id), ]; + + final List favs = (await bind.mainGetFav()).toList(); + if (isDesktop && peer.platform != 'Android') { menuItems.add(_tcpTunnelingAction(context, peer.id)); } @@ -705,7 +708,13 @@ class RecentPeerCard extends BasePeerCard { if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } - menuItems.add(_addFavAction(peer.id)); + + if (!favs.contains(peer.id)) { + menuItems.add(_addFavAction(peer.id)); + } else { + menuItems.add(_rmFavAction(peer.id, () async {})); + } + if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } From 819dc4e1a9643df899375240b217dcaccb4a8fd8 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 17 Feb 2023 22:37:08 +0100 Subject: [PATCH 186/202] PeerCard. Menu. "add to favorites" visual indicator --- flutter/lib/common/widgets/peer_card.dart | 36 +++++++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 9d9d3d01f..fd0499305 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -565,9 +565,21 @@ abstract class BasePeerCard extends StatelessWidget { @protected MenuEntryBase _addFavAction(String id) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Add to Favorites'), - style: style, + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Add to Favorites'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.star_outline), + ), + ).marginOnly(right: 4)), + ], ), proc: () { () async { @@ -587,9 +599,21 @@ abstract class BasePeerCard extends StatelessWidget { MenuEntryBase _rmFavAction( String id, Future Function() reloadFunc) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Remove from Favorites'), - style: style, + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Remove from Favorites'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.star), + ), + ).marginOnly(right: 4)), + ], ), proc: () { () async { From b98581303e7e62f3dcdbfb04737f520345e0fc5d Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 23 Feb 2023 13:39:01 +0100 Subject: [PATCH 187/202] PeerCard. Menu. Hide "Add to Addressbook" if not logged in --- flutter/lib/common/widgets/peer_card.dart | 28 +++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index fd0499305..325dfd2ed 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -739,9 +739,15 @@ class RecentPeerCard extends BasePeerCard { menuItems.add(_rmFavAction(peer.id, () async {})); } - if (!gFFI.abModel.idContainBy(peer.id)) { + if (gFFI.userModel.userName.isNotEmpty) { + // if (!gFFI.abModel.idContainBy(peer.id)) { + // menuItems.add(_addToAb(peer)); + // } else { + // menuItems.add(_removeFromAb(peer)); + // } menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadRecentPeers(); @@ -784,9 +790,16 @@ class FavoritePeerCard extends BasePeerCard { menuItems.add(_rmFavAction(peer.id, () async { await bind.mainLoadFavPeers(); })); - if (!gFFI.abModel.idContainBy(peer.id)) { + + if (gFFI.userModel.userName.isNotEmpty) { + // if (!gFFI.abModel.idContainBy(peer.id)) { + // menuItems.add(_addToAb(peer)); + // } else { + // menuItems.add(_removeFromAb(peer)); + // } menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadFavPeers(); @@ -821,10 +834,16 @@ class DiscoveredPeerCard extends BasePeerCard { if (Platform.isWindows) { menuItems.add(_createShortCutAction(peer.id)); } - menuItems.add(MenuEntryDivider()); - if (!gFFI.abModel.idContainBy(peer.id)) { + + if (gFFI.userModel.userName.isNotEmpty) { + // if (!gFFI.abModel.idContainBy(peer.id)) { + // menuItems.add(_addToAb(peer)); + // } else { + // menuItems.add(_removeFromAb(peer)); + // } menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); return menuItems; @@ -865,6 +884,7 @@ class AddressBookPeerCard extends BasePeerCard { if (gFFI.abModel.tags.isNotEmpty) { menuItems.add(_editTagAction(peer.id)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); return menuItems; From 27b8df617d2d0b4fa8ac851ea73851597aefb94c Mon Sep 17 00:00:00 2001 From: grummbeer Date: Mon, 20 Feb 2023 20:13:36 +0100 Subject: [PATCH 188/202] PeerCard. Menu. Remove peer also from favorites when deleted --- flutter/lib/common/widgets/peer_card.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 325dfd2ed..657ba3ccf 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -536,6 +536,10 @@ abstract class BasePeerCard extends StatelessWidget { if (isLan) { // TODO } else { + final favs = (await bind.mainGetFav()).toList(); + if (favs.remove(id)) { + await bind.mainStoreFav(favs: favs); + } await bind.mainRemovePeer(id: id); } removePreference(id); From 0739820774c92e5d0688ec7397c559a720becf69 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Tue, 21 Feb 2023 15:58:00 +0100 Subject: [PATCH 189/202] PeerCard. Menu. Add menu item "add to favorite" to DiscoveredPeerCard --- flutter/lib/common/widgets/peer_card.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 657ba3ccf..db2a90d9e 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -827,6 +827,9 @@ class DiscoveredPeerCard extends BasePeerCard { _connectAction(context, peer), _transferFileAction(context, peer.id), ]; + + final List favs = (await bind.mainGetFav()).toList(); + if (isDesktop && peer.platform != 'Android') { menuItems.add(_tcpTunnelingAction(context, peer.id)); } @@ -839,6 +842,12 @@ class DiscoveredPeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } + if (!favs.contains(peer.id)) { + menuItems.add(_addFavAction(peer.id)); + } else { + menuItems.add(_rmFavAction(peer.id, () async {})); + } + if (gFFI.userModel.userName.isNotEmpty) { // if (!gFFI.abModel.idContainBy(peer.id)) { // menuItems.add(_addToAb(peer)); From 8c3be1c8ced9eba83d682c02ac15bdfbdeaeb840 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Tue, 21 Feb 2023 18:01:43 +0100 Subject: [PATCH 190/202] PeerCard. Menu. Add label to text input on "rename" --- flutter/lib/common/widgets/peer_card.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index db2a90d9e..8d4d58772 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -682,8 +682,9 @@ abstract class BasePeerCard extends StatelessWidget { child: TextFormField( controller: controller, autofocus: true, - decoration: - const InputDecoration(border: OutlineInputBorder()), + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: translate('Name')), ), ), ), From 37deaf67ccc62fdba61eff2ade71d14ef26d116f Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Thu, 23 Feb 2023 14:53:24 +0100 Subject: [PATCH 191/202] fix back icon --- flutter/lib/desktop/pages/file_manager_page.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 0d55552af..39d66f568 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -752,9 +752,12 @@ class _FileManagerPageState extends State padding: EdgeInsets.only( right: 3, ), - child: SvgPicture.asset( - "assets/arrow.svg", - color: Theme.of(context).tabBarTheme.labelColor, + child: RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + "assets/arrow.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), ), color: Theme.of(context).cardColor, hoverColor: Theme.of(context).hoverColor, From 135e0c8a99be4e9c629110bb65ee6e62cc8b45a3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 21:57:51 +0800 Subject: [PATCH 192/202] add mutex guard for arboard funcs Signed-off-by: fufesou --- src/common.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index 02d367b5e..5f24fd5c3 100644 --- a/src/common.rs +++ b/src/common.rs @@ -52,6 +52,11 @@ lazy_static::lazy_static! { pub static ref DEVICE_NAME: Arc> = Default::default(); } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +lazy_static::lazy_static! { + static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); +} + pub fn global_init() -> bool { #[cfg(target_os = "linux")] { @@ -96,7 +101,11 @@ pub fn check_clipboard( ) -> Option { let side = if old.is_none() { "host" } else { "client" }; let old = if let Some(old) = old { old } else { &CONTENT }; - if let Ok(content) = ctx.get_text() { + let content = { + let _lock = ARBOARD_MTX.lock().unwrap(); + ctx.get_text() + }; + if let Ok(content) = content { if content.len() < 2_000_000 && !content.is_empty() { let changed = content != *old.lock().unwrap(); if changed { @@ -174,6 +183,7 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) let side = if old.is_none() { "host" } else { "client" }; let old = if let Some(old) = old { old } else { &CONTENT }; *old.lock().unwrap() = content.clone(); + let _lock = ARBOARD_MTX.lock().unwrap(); allow_err!(ctx.set_text(content)); log::debug!("{} updated on {}", CLIPBOARD_NAME, side); } From ab9acc76fce569a984b7216121c41b5544c4f07b Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Thu, 23 Feb 2023 16:49:31 +0100 Subject: [PATCH 193/202] backgroundcolor migration --- flutter/lib/common.dart | 15 ++++++++++----- flutter/lib/common/widgets/chat_page.dart | 6 ++++-- flutter/lib/common/widgets/peer_card.dart | 8 ++++---- flutter/lib/common/widgets/peer_tab_page.dart | 7 ++++--- flutter/lib/desktop/pages/connection_page.dart | 2 +- .../lib/desktop/pages/desktop_home_page.dart | 4 ++-- .../lib/desktop/pages/desktop_setting_page.dart | 2 +- flutter/lib/desktop/pages/desktop_tab_page.dart | 2 +- .../lib/desktop/pages/port_forward_page.dart | 17 +++++++++-------- .../desktop/pages/port_forward_tab_page.dart | 2 +- flutter/lib/desktop/pages/remote_page.dart | 2 +- flutter/lib/desktop/pages/remote_tab_page.dart | 2 +- flutter/lib/desktop/pages/server_page.dart | 9 +++------ 13 files changed, 42 insertions(+), 36 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6d3e4c3b7..e1b9ac90c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -45,9 +45,10 @@ var isWeb = false; var isWebDesktop = false; var version = ""; int androidVersion = 0; + /// Incriment count for textureId. int _textureId = 0; -int get newTextureId => _textureId ++; +int get newTextureId => _textureId++; final textureRenderer = TextureRgbaRenderer(); /// only available for Windows target @@ -165,7 +166,6 @@ class MyTheme { static ThemeData lightTheme = ThemeData( brightness: Brightness.light, - backgroundColor: Color(0xFFEEEEEE), hoverColor: Color.fromARGB(255, 224, 224, 224), scaffoldBackgroundColor: Color(0xFFFFFFFF), textTheme: const TextTheme( @@ -177,7 +177,6 @@ class MyTheme { labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)), cardColor: Color(0xFFEEEEEE), hintColor: Color(0xFFAAAAAA), - primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( labelColor: Colors.black87, @@ -190,6 +189,10 @@ class MyTheme { style: ButtonStyle(splashFactory: NoSplash.splashFactory), ) : null, + colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue).copyWith( + brightness: Brightness.light, + background: Color(0xFFEEEEEE), + ), ).copyWith( extensions: >[ ColorThemeExtension.light, @@ -198,7 +201,6 @@ class MyTheme { ); static ThemeData darkTheme = ThemeData( brightness: Brightness.dark, - backgroundColor: Color(0xFF24252B), hoverColor: Color.fromARGB(255, 45, 46, 53), scaffoldBackgroundColor: Color(0xFF18191E), textTheme: const TextTheme( @@ -209,7 +211,6 @@ class MyTheme { labelLarge: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, color: accent80)), cardColor: Color(0xFF24252B), - primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( labelColor: Colors.white70, @@ -227,6 +228,10 @@ class MyTheme { : null, checkboxTheme: const CheckboxThemeData(checkColor: MaterialStatePropertyAll(dark)), + colorScheme: ColorScheme.fromSwatch( + brightness: Brightness.dark, + primarySwatch: Colors.blue, + ).copyWith(background: Color(0xFF24252B)), ).copyWith( extensions: >[ ColorThemeExtension.dark, diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index 62f81b797..c1991633a 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -75,7 +75,8 @@ class ChatPage extends StatelessWidget implements PageShape { hintText: "${translate('Write a message')}...", filled: true, - fillColor: Theme.of(context).backgroundColor, + fillColor: + Theme.of(context).colorScheme.background, contentPadding: EdgeInsets.all(10), border: OutlineInputBorder( borderRadius: BorderRadius.circular(6), @@ -88,7 +89,8 @@ class ChatPage extends StatelessWidget implements PageShape { : defaultInputDecoration( hintText: "${translate('Write a message')}...", - fillColor: Theme.of(context).backgroundColor), + fillColor: + Theme.of(context).colorScheme.background), sendButtonBuilder: defaultSendButton( padding: EdgeInsets.symmetric( horizontal: 6, vertical: 0), diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index f1b94ecdf..0a175139f 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -170,8 +170,8 @@ class _PeerCardState extends State<_PeerCard> ), Expanded( child: Container( - decoration: - BoxDecoration(color: Theme.of(context).backgroundColor), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background), child: Row( children: [ Expanded( @@ -266,7 +266,7 @@ class _PeerCardState extends State<_PeerCard> ), ), Container( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -1090,7 +1090,7 @@ class ActionMore extends StatelessWidget { radius: 14, backgroundColor: _hover.value ? Theme.of(context).scaffoldBackgroundColor - : Theme.of(context).backgroundColor, + : Theme.of(context).colorScheme.background, child: Icon(Icons.more_vert, size: 18, color: _hover.value diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 4080f9c11..da7e37e6b 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -156,7 +156,7 @@ class _PeerTabPageState extends State padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: model.currentTab == t - ? Theme.of(context).backgroundColor + ? Theme.of(context).colorScheme.background : null, borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), ), @@ -231,7 +231,8 @@ class _PeerTabPageState extends State Widget _createPeerViewTypeSwitch(BuildContext context) { final textColor = Theme.of(context).textTheme.titleLarge?.color; - final activeDeco = BoxDecoration(color: Theme.of(context).backgroundColor); + final activeDeco = + BoxDecoration(color: Theme.of(context).colorScheme.background); return Row( children: [PeerUiType.grid, PeerUiType.list] .map((type) => Obx( @@ -351,7 +352,7 @@ class _PeerSearchBarState extends State { return Container( width: 120, decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, borderRadius: BorderRadius.circular(6), ), child: Obx(() => Row( diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 646ee2a8d..4aad66eee 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -164,7 +164,7 @@ class _ConnectionPageState extends State width: 320 + 20 * 2, padding: const EdgeInsets.fromLTRB(20, 24, 20, 22), decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, borderRadius: const BorderRadius.all(Radius.circular(13)), ), child: Ink( diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index ff99c9dc8..dfa5762b0 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -71,7 +71,7 @@ class _DesktopHomePageState extends State value: gFFI.serverModel, child: Container( width: 200, - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, child: DesktopScrollWrapper( scrollController: _leftPaneScrollController, child: SingleChildScrollView( @@ -185,7 +185,7 @@ class _DesktopHomePageState extends State radius: 15, backgroundColor: hover.value ? Theme.of(context).scaffoldBackgroundColor - : Theme.of(context).backgroundColor, + : Theme.of(context).colorScheme.background, child: Icon( Icons.more_vert_outlined, size: 20, diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 971c713ce..06a79093a 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -108,7 +108,7 @@ class _DesktopSettingPageState extends State Widget build(BuildContext context) { super.build(context); return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: Row( children: [ SizedBox( diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 35d5a61ef..053a2d8a2 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -65,7 +65,7 @@ class _DesktopTabPageState extends State { Widget build(BuildContext context) { final tabWidget = Container( child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: DesktopTab( controller: tabController, tail: ActionIcon( diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 2ac6bf23a..ae070b47b 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -91,7 +91,7 @@ class _PortForwardPageState extends State Flexible( child: Container( decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, border: Border.all(width: 1, color: MyTheme.border)), child: widget.isRDP ? buildRdp(context) : buildTunnel(context), @@ -134,7 +134,7 @@ class _PortForwardPageState extends State return Theme( data: Theme.of(context) - .copyWith(backgroundColor: Theme.of(context).backgroundColor), + .copyWith(backgroundColor: Theme.of(context).colorScheme.background), child: Obx(() => ListView.builder( controller: ScrollController(), itemCount: pfs.length + 2, @@ -169,7 +169,8 @@ class _PortForwardPageState extends State return Container( height: _kRowHeight, - decoration: BoxDecoration(color: Theme.of(context).backgroundColor), + decoration: + BoxDecoration(color: Theme.of(context).colorScheme.background), child: Row(children: [ buildTunnelInputCell(context, controller: localPortController, @@ -229,7 +230,7 @@ class _PortForwardPageState extends State borderSide: BorderSide(color: MyTheme.color(context).border!)), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: MyTheme.color(context).border!)), - fillColor: Theme.of(context).backgroundColor, + fillColor: Theme.of(context).colorScheme.background, contentPadding: const EdgeInsets.all(10), hintText: hint, hintStyle: @@ -251,7 +252,7 @@ class _PortForwardPageState extends State ? MyTheme.currentThemeMode() == ThemeMode.dark ? const Color(0xFF202020) : const Color(0xFFF4F5F6) - : Theme.of(context).backgroundColor), + : Theme.of(context).colorScheme.background), child: Row(children: [ text(pf.localPort.toString()), const SizedBox(width: _kColumn1Width), @@ -293,7 +294,7 @@ class _PortForwardPageState extends State ).marginOnly(left: _kTextLeftMargin)); return Theme( data: Theme.of(context) - .copyWith(backgroundColor: Theme.of(context).backgroundColor), + .copyWith(backgroundColor: Theme.of(context).colorScheme.background), child: ListView.builder( controller: ScrollController(), itemCount: 2, @@ -312,8 +313,8 @@ class _PortForwardPageState extends State } else { return Container( height: _kRowHeight, - decoration: - BoxDecoration(color: Theme.of(context).backgroundColor), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background), child: Row(children: [ Expanded( child: Align( diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index ee5dd9b53..f2d75d00f 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -96,7 +96,7 @@ class _PortForwardTabPageState extends State { decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: DesktopTab( controller: tabController, onWindowCloseButton: () async { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index e52334512..ab0daece7 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -225,7 +225,7 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay /// see override build() in [BlockableOverlay] diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index ef3a0dd04..0deb646c0 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -141,7 +141,7 @@ class _ConnectionTabPageState extends State { width: stateGlobal.windowBorderWidth.value), ), child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 252e1cd12..45591b79b 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -49,10 +49,7 @@ class _DesktopServerPageState extends State @override void onWindowClose() { - Future.wait([ - gFFI.serverModel.closeAll(), - gFFI.close() - ]).then((_) { + Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) { if (Platform.isMacOS) { RdPlatformChannel.instance.terminate(); } else { @@ -82,7 +79,7 @@ class _DesktopServerPageState extends State decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -189,7 +186,7 @@ class ConnectionManagerState extends State { windowManager.startDragging(); }, child: Container( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, ), ), ), From 080c98769437e0a931fc9073bf309e000e19a075 Mon Sep 17 00:00:00 2001 From: jimmyGALLAND <64364019+jimmyGALLAND@users.noreply.github.com> Date: Thu, 23 Feb 2023 22:17:59 +0100 Subject: [PATCH 194/202] Update fr.rs --- src/lang/fr.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 1f6e9f55b..50cb29938 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -37,10 +37,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Presse-papier vide"), ("Stop service", "Arrêter le service"), ("Change ID", "Changer d'ID"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Votre nouvel ID"), + ("length %min% to %max%", "longueur de %min% à %max%"), + ("starts with a letter", "commence par une lettre"), + ("allowed characters", "caractères autorisés"), ("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur doit être comprise entre 6 et 16."), ("Website", "Site Web"), ("About", "À propos de"), @@ -89,7 +89,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show Hidden Files", "Afficher les fichiers cachés"), ("Receive", "Recevoir"), ("Send", "Envoyer"), - ("Refresh File", "Actualiser le fichier"), + ("Refresh File", "Rafraîchir le contenu"), ("Local", "Local"), ("Remote", "Distant"), ("Remote Computer", "Ordinateur distant"), From 91a2a5b56e283b208a3822c6fccd81c3fb8ea599 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 9 Feb 2023 15:53:51 +0800 Subject: [PATCH 195/202] win resolution && api Signed-off-by: 21pages --- flutter/lib/models/model.dart | 41 +++++++++++++ libs/hbb_common/protos/message.proto | 10 ++++ src/flutter.rs | 25 ++++++++ src/flutter_ffi.rs | 8 ++- src/platform/mod.rs | 10 +++- src/platform/windows.rs | 90 +++++++++++++++++++++++++++- src/server/connection.rs | 48 +++++++++++++++ src/server/video_service.rs | 15 ++++- src/ui_session_interface.rs | 12 ++++ 9 files changed, 255 insertions(+), 4 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 5ef72a0af..f4efe2f08 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -270,6 +270,7 @@ class FfiModel with ChangeNotifier { parent.target?.canvasModel.updateViewStyle(); } parent.target?.recordingModel.onSwitchDisplay(); + handleResolutions(peerId, evt["resolutions"]); notifyListeners(); } @@ -437,10 +438,35 @@ class FfiModel with ChangeNotifier { } Map features = json.decode(evt['features']); _pi.features.privacyMode = features['privacy_mode'] == 1; + handleResolutions(peerId, evt["resolutions"]); } notifyListeners(); } + handleResolutions(String id, dynamic resolutions) { + try { + final List dynamicArray = jsonDecode(resolutions as String); + List arr = List.empty(growable: true); + for (int i = 0; i < dynamicArray.length; i++) { + var width = dynamicArray[i]["width"]; + var height = dynamicArray[i]["height"]; + if (width is int && width > 0 && height is int && height > 0) { + arr.add(Resolution(width, height)); + } + } + arr.sort((a, b) { + if (b.width != a.width) { + return b.width - a.width; + } else { + return b.height - a.height; + } + }); + _pi.resolutions = arr; + } catch (e) { + debugPrint("Failed to parse resolutions:$e"); + } + } + /// Handle the peer info synchronization event based on [evt]. handleSyncPeerInfo(Map evt, String peerId) async { if (evt['displays'] != null) { @@ -458,6 +484,9 @@ class FfiModel with ChangeNotifier { } _pi.displays = newDisplays; stateGlobal.displaysCount.value = _pi.displays.length; + if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) { + _display = _pi.displays[_pi.currentDisplay]; + } } notifyListeners(); } @@ -1532,6 +1561,17 @@ class Display { } } +class Resolution { + int width = 0; + int height = 0; + Resolution(this.width, this.height); + + @override + String toString() { + return 'Resolution($width,$height)'; + } +} + class Features { bool privacyMode = false; } @@ -1545,6 +1585,7 @@ class PeerInfo { int currentDisplay = 0; List displays = []; Features features = Features(); + List resolutions = []; } const canvasKey = 'canvas'; diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 2a3fd05b4..be3a1e51e 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -90,6 +90,7 @@ message PeerInfo { int32 conn_id = 8; Features features = 9; SupportedEncoding encoding = 10; + SupportedResolutions resolutions = 11; } message LoginResponse { @@ -416,6 +417,13 @@ message Cliprdr { } } +message Resolution { + int32 width = 1; + int32 height = 2; +} + +message SupportedResolutions { repeated Resolution resolutions = 1; } + message SwitchDisplay { int32 display = 1; sint32 x = 2; @@ -423,6 +431,7 @@ message SwitchDisplay { int32 width = 4; int32 height = 5; bool cursor_embedded = 6; + SupportedResolutions resolutions = 7; } message PermissionInfo { @@ -597,6 +606,7 @@ message Misc { bool portable_service_running = 20; SwitchSidesRequest switch_sides_request = 21; SwitchBack switch_back = 22; + Resolution change_resolution = 24; } } diff --git a/src/flutter.rs b/src/flutter.rs index d366a0eda..ea73eb925 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -480,6 +480,7 @@ impl InvokeUiSession for FlutterHandler { features.insert("privacy_mode", 0); } let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); + let resolutions = serialize_resolutions(&pi.resolutions.resolutions); *self.peer_info.write().unwrap() = pi.clone(); self.push_event( "peer_info", @@ -492,6 +493,7 @@ impl InvokeUiSession for FlutterHandler { ("version", &pi.version), ("features", &features), ("current_display", &pi.current_display.to_string()), + ("resolutions", &resolutions), ], ); } @@ -529,6 +531,7 @@ impl InvokeUiSession for FlutterHandler { } fn switch_display(&self, display: &SwitchDisplay) { + let resolutions = serialize_resolutions(&display.resolutions.resolutions); self.push_event( "switch_display", vec![ @@ -548,6 +551,7 @@ impl InvokeUiSession for FlutterHandler { } .to_string(), ), + ("resolutions", &resolutions), ], ); } @@ -861,6 +865,27 @@ pub fn set_cur_session_id(id: String) { } } +#[inline] +fn serialize_resolutions(resolutions: &Vec) -> String { + #[derive(Debug, serde::Serialize)] + struct ResolutionSerde { + width: i32, + height: i32, + } + + let mut v = vec![]; + resolutions + .iter() + .map(|r| { + v.push(ResolutionSerde { + width: r.width, + height: r.height, + }) + }) + .count(); + serde_json::ser::to_string(&v).unwrap_or("".to_string()) +} + #[no_mangle] #[cfg(not(feature = "flutter_texture_render"))] pub fn session_get_rgba_size(id: *const char) -> usize { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 14906d568..8a8bf4de4 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -529,7 +529,13 @@ pub fn session_switch_sides(id: String) { } } -pub fn session_set_size(_id: String, _width: i32, _height: i32) { +pub fn session_change_resolution(id: String, width: i32, height: i32) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.change_resolution(width, height); + } +} + +pub fn session_set_size(_id: String, _width: i32, _height: i32) { #[cfg(feature = "flutter_texture_render")] if let Some(session) = SESSIONS.write().unwrap().get_mut(&_id) { session.set_size(_width, _height); diff --git a/src/platform/mod.rs b/src/platform/mod.rs index ed5fcfaa1..ad058d4c0 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -74,5 +74,13 @@ mod tests { assert!(!get_cursor_pos().is_none()); } } -} + #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[test] + fn test_resolution() { + let name = r"\\.\DISPLAY1"; + println!("current:{:?}", current_resolution(name)); + println!("change:{:?}", change_resolution(name, 2880, 1800)); + println!("resolutions:{:?}", resolutions(name)); + } +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index bd6a1fc4c..6b3f8013c 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -5,7 +5,9 @@ use crate::license::*; use hbb_common::{ allow_err, bail, config::{self, Config}, - log, sleep, timeout, tokio, + log, + message_proto::Resolution, + sleep, timeout, tokio, }; use std::io::prelude::*; use std::{ @@ -1784,3 +1786,89 @@ pub fn set_path_permission(dir: &PathBuf, permission: &str) -> ResultType<()> { .spawn()?; Ok(()) } + +pub fn resolutions(name: &str) -> Vec { + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + let wname = wide_string(name); + let len = if wname.len() <= dm.dmDeviceName.len() { + wname.len() + } else { + dm.dmDeviceName.len() + }; + std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len); + dm.dmSize = std::mem::size_of::() as _; + let mut v = vec![]; + let mut num = 0; + loop { + if EnumDisplaySettingsW(NULL as _, num, &mut dm) == 0 { + break; + } + let r = Resolution { + width: dm.dmPelsWidth as _, + height: dm.dmPelsHeight as _, + ..Default::default() + }; + if !v.contains(&r) { + v.push(r); + } + num += 1; + } + v + } +} + +pub fn current_resolution(name: &str) -> ResultType { + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + dm.dmSize = std::mem::size_of::() as _; + let wname = wide_string(name); + if EnumDisplaySettingsW(wname.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm) == 0 { + bail!( + "failed to get currrent resolution, errno={}", + GetLastError() + ); + } + let r = Resolution { + width: dm.dmPelsWidth as _, + height: dm.dmPelsHeight as _, + ..Default::default() + }; + Ok(r) + } +} + +pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + if FALSE == EnumDisplaySettingsW(NULL as _, ENUM_CURRENT_SETTINGS, &mut dm) { + bail!("EnumDisplaySettingsW failed, errno={}", GetLastError()); + } + let wname = wide_string(name); + let len = if wname.len() <= dm.dmDeviceName.len() { + wname.len() + } else { + dm.dmDeviceName.len() + }; + std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len); + dm.dmSize = std::mem::size_of::() as _; + dm.dmPelsWidth = width as _; + dm.dmPelsHeight = height as _; + dm.dmFields = DM_PELSHEIGHT | DM_PELSWIDTH; + let res = ChangeDisplaySettingsExW( + wname.as_ptr(), + &mut dm, + NULL as _, + CDS_UPDATEREGISTRY | CDS_GLOBAL | CDS_RESET, + NULL, + ); + if res != DISP_CHANGE_SUCCESSFUL { + bail!( + "ChangeDisplaySettingsExW failed, res={}, errno={}", + res, + GetLastError() + ); + } + Ok(()) + } +} diff --git a/src/server/connection.rs b/src/server/connection.rs index d2eb21ee5..85fcb676b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -123,6 +123,7 @@ pub struct Connection { #[cfg(windows)] portable: PortableState, from_switch: bool, + origin_resolution: HashMap, voice_call_request_timestamp: Option, audio_input_device_before_voice_call: Option, } @@ -228,6 +229,7 @@ impl Connection { #[cfg(windows)] portable: Default::default(), from_switch: false, + origin_resolution: Default::default(), audio_sender: None, voice_call_request_timestamp: None, audio_input_device_before_voice_call: None, @@ -533,6 +535,8 @@ impl Connection { conn.post_conn_audit(json!({ "action": "close", })); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + conn.reset_resolution(); ALIVE_CONNS.lock().unwrap().retain(|&c| c != id); if let Some(s) = conn.server.upgrade() { s.write().unwrap().remove_connection(&conn.inner); @@ -881,6 +885,16 @@ impl Connection { ..Default::default() }) .into(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + pi.resolutions = Some(SupportedResolutions { + resolutions: video_service::get_current_display_name() + .map(|name| crate::platform::resolutions(&name)) + .unwrap_or(vec![]), + ..Default::default() + }) + .into(); + } let mut sub_service = false; if self.file_transfer.is_some() { @@ -1597,6 +1611,26 @@ impl Connection { return false; } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::ChangeResolution(r)) => { + if self.keyboard { + if let Ok(name) = video_service::get_current_display_name() { + if let Ok(current) = crate::platform::current_resolution(&name) { + if let Err(e) = crate::platform::change_resolution( + &name, + r.width as _, + r.height as _, + ) { + log::error!("change resolution failed:{:?}", e); + } else { + if !self.origin_resolution.contains_key(&name) { + self.origin_resolution.insert(name, current); + } + } + } + } + } + } _ => {} }, Some(message::Union::AudioFrame(frame)) => { @@ -1937,6 +1971,20 @@ impl Connection { } } } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn reset_resolution(&self) { + self.origin_resolution + .iter() + .map(|(name, r)| { + if let Err(e) = + crate::platform::change_resolution(&name, r.width as _, r.height as _) + { + log::error!("change resolution failed:{:?}", e); + } + }) + .count(); + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 52b1717c4..a9a9fd9ab 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -356,7 +356,7 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType ResultType ResultType<()> { width: c.width as _, height: c.height as _, cursor_embedded: capture_cursor_embedded(), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + resolutions: Some(SupportedResolutions { + resolutions: get_current_display_name() + .map(|name| crate::platform::resolutions(&name)) + .unwrap_or(vec![]), + ..SupportedResolutions::default() + }) + .into(), ..Default::default() }); let mut msg_out = Message::new(); @@ -992,6 +1001,10 @@ pub fn get_current_display() -> ResultType<(usize, usize, Display)> { get_current_display_2(try_get_displays()?) } +pub fn get_current_display_name() -> ResultType { + Ok(get_current_display_2(try_get_displays()?)?.2.name()) +} + #[cfg(windows)] fn start_uac_elevation_check() { static START: Once = Once::new(); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 5fbf2f4e7..fd5a7d9c0 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -713,6 +713,18 @@ impl Session { } } + pub fn change_resolution(&self, width: i32, height: i32) { + let mut misc = Misc::new(); + misc.set_change_resolution(Resolution { + width, + height, + ..Default::default() + }); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(Data::Message(msg)); + } + pub fn request_voice_call(&self) { self.send(Data::NewVoiceCall); } From 18a66749a1a7d0b767cb05f197bebc309c7bcbd0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 11 Feb 2023 19:04:33 +0800 Subject: [PATCH 196/202] linux x11 resolution Signed-off-by: 21pages --- Cargo.lock | 72 +++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + libs/scrap/src/common/x11.rs | 4 +- libs/scrap/src/x11/display.rs | 7 ++++ libs/scrap/src/x11/ffi.rs | 18 +++++++++ libs/scrap/src/x11/iter.rs | 30 +++++++++++++++ src/platform/linux.rs | 55 +++++++++++++++++++++++++- 7 files changed, 180 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8483cbac1..a2cdf91a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1159,14 +1159,38 @@ dependencies = [ "zvariant", ] +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core 0.10.2", + "darling_macro 0.10.2", +] + [[package]] name = "darling" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2 1.0.47", + "quote 1.0.21", + "strsim 0.9.3", + "syn 1.0.105", ] [[package]] @@ -1183,13 +1207,24 @@ dependencies = [ "syn 1.0.105", ] +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core 0.10.2", + "quote 1.0.21", + "syn 1.0.105", +] + [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", + "darling_core 0.13.4", "quote 1.0.21", "syn 1.0.105", ] @@ -1389,6 +1424,18 @@ dependencies = [ "syn 1.0.105", ] +[[package]] +name = "derive_setters" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1cf41b4580a37cca5ef2ada2cc43cf5d6be3983f4522e83010d67ab6925e84b" +dependencies = [ + "darling 0.10.2", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", +] + [[package]] name = "detect-desktop-environment" version = "0.2.0" @@ -3585,7 +3632,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro-crate 1.2.1", "proc-macro2 1.0.47", "quote 1.0.21", @@ -4944,6 +4991,7 @@ dependencies = [ "winreg 0.10.1", "winres", "wol-rs", + "xrandr-parser", ] [[package]] @@ -5469,6 +5517,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "strsim" version = "0.10.0" @@ -6965,6 +7019,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +[[package]] +name = "xrandr-parser" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5af43ba661cee58bd86b9f81a899e45a15ac7f42fa4401340f73c0c2950030c1" +dependencies = [ + "derive_setters", + "serde 1.0.149", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index c20366983..f93f776a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,6 +121,7 @@ mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } dbus = "0.9" dbus-crossroads = "0.5" +xrandr-parser = "0.3.0" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs index 61112bff7..6e3fc94fb 100644 --- a/libs/scrap/src/common/x11.rs +++ b/libs/scrap/src/common/x11.rs @@ -1,4 +1,4 @@ -use crate::{x11, common::TraitCapturer}; +use crate::{common::TraitCapturer, x11}; use std::{io, ops, time::Duration}; pub struct Capturer(x11::Capturer); @@ -90,6 +90,6 @@ impl Display { } pub fn name(&self) -> String { - "".to_owned() + self.0.name() } } diff --git a/libs/scrap/src/x11/display.rs b/libs/scrap/src/x11/display.rs index 0c5ba5035..a33903caa 100644 --- a/libs/scrap/src/x11/display.rs +++ b/libs/scrap/src/x11/display.rs @@ -9,6 +9,7 @@ pub struct Display { default: bool, rect: Rect, root: xcb_window_t, + name: String, } #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] @@ -25,12 +26,14 @@ impl Display { default: bool, rect: Rect, root: xcb_window_t, + name: String, ) -> Display { Display { server, default, rect, root, + name, } } @@ -52,4 +55,8 @@ impl Display { pub fn root(&self) -> xcb_window_t { self.root } + + pub fn name(&self) -> String { + self.name.clone() + } } diff --git a/libs/scrap/src/x11/ffi.rs b/libs/scrap/src/x11/ffi.rs index 500f57615..b34fed416 100644 --- a/libs/scrap/src/x11/ffi.rs +++ b/libs/scrap/src/x11/ffi.rs @@ -65,6 +65,21 @@ extern "C" { ) -> xcb_randr_monitor_info_iterator_t; pub fn xcb_randr_monitor_info_next(i: *mut xcb_randr_monitor_info_iterator_t); + + pub fn xcb_get_atom_name( + c: *mut xcb_connection_t, + atom: xcb_atom_t, + ) -> xcb_get_atom_name_cookie_t; + + pub fn xcb_get_atom_name_reply( + c: *mut xcb_connection_t, + cookie: xcb_get_atom_name_cookie_t, + e: *mut *mut xcb_generic_error_t, + ) -> *const xcb_get_atom_name_reply_t; + + pub fn xcb_get_atom_name_name(reply: *const xcb_get_atom_name_request_t) -> *const u8; + + pub fn xcb_get_atom_name_name_length(reply: *const xcb_get_atom_name_reply_t) -> i32; } pub const XCB_IMAGE_FORMAT_Z_PIXMAP: u8 = 2; @@ -78,6 +93,9 @@ pub type xcb_timestamp_t = u32; pub type xcb_colormap_t = u32; pub type xcb_shm_seg_t = u32; pub type xcb_drawable_t = u32; +pub type xcb_get_atom_name_cookie_t = u32; +pub type xcb_get_atom_name_reply_t = u32; +pub type xcb_get_atom_name_request_t = xcb_get_atom_name_reply_t; #[repr(C)] pub struct xcb_setup_t { diff --git a/libs/scrap/src/x11/iter.rs b/libs/scrap/src/x11/iter.rs index 406c27352..28609376b 100644 --- a/libs/scrap/src/x11/iter.rs +++ b/libs/scrap/src/x11/iter.rs @@ -1,3 +1,4 @@ +use std::ffi::CString; use std::ptr; use std::rc::Rc; @@ -64,6 +65,7 @@ impl Iterator for DisplayIter { if inner.rem != 0 { unsafe { let data = &*inner.data; + let name = get_atom_name(self.server.raw(), data.name); let display = Display::new( self.server.clone(), @@ -75,6 +77,7 @@ impl Iterator for DisplayIter { h: data.height, }, root, + name, ); xcb_randr_monitor_info_next(inner); @@ -91,3 +94,30 @@ impl Iterator for DisplayIter { } } } + +fn get_atom_name(conn: *mut xcb_connection_t, atom: xcb_atom_t) -> String { + let empty = "".to_owned(); + if atom == 0 { + return empty; + } + unsafe { + let mut e: xcb_generic_error_t = std::mem::zeroed(); + let reply = xcb_get_atom_name_reply( + conn, + xcb_get_atom_name(conn, atom), + &mut ((&mut e) as *mut xcb_generic_error_t) as _, + ); + if reply == std::ptr::null() { + return empty; + } + let length = xcb_get_atom_name_name_length(reply); + let name = xcb_get_atom_name_name(reply); + let mut v = vec![0u8; length as _]; + std::ptr::copy_nonoverlapping(name as _, v.as_mut_ptr(), length as _); + libc::free(reply as *mut _); + if let Ok(s) = CString::new(v) { + return s.to_string_lossy().to_string(); + } + empty + } +} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 32c32efb9..08e343d49 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,7 +1,7 @@ use super::{CursorData, ResultType}; use hbb_common::libc::{c_char, c_int, c_long, c_void}; pub use hbb_common::platform::linux::*; -use hbb_common::{allow_err, bail, log}; +use hbb_common::{allow_err, anyhow::anyhow, bail, log, message_proto::Resolution}; use std::{ cell::RefCell, path::PathBuf, @@ -10,6 +10,7 @@ use std::{ Arc, }, }; +use xrandr_parser::Parser; type Xdo = *const c_void; @@ -641,3 +642,55 @@ pub fn get_double_click_time() -> u32 { double_click_time } } + +pub fn resolutions(name: &str) -> Vec { + let mut v = vec![]; + let mut parser = Parser::new(); + if parser.parse().is_ok() { + if let Ok(connector) = parser.get_connector(name) { + if let Ok(resolutions) = &connector.available_resolutions() { + for r in resolutions { + if let Ok(width) = r.horizontal.parse::() { + if let Ok(height) = r.vertical.parse::() { + let resolution = Resolution { + width, + height, + ..Default::default() + }; + if !v.contains(&resolution) { + v.push(resolution); + } + } + } + } + } + } + } + v +} + +pub fn current_resolution(name: &str) -> ResultType { + let mut parser = Parser::new(); + parser.parse().map_err(|e| anyhow!(e))?; + let connector = parser.get_connector(name).map_err(|e| anyhow!(e))?; + let r = connector.current_resolution(); + let width = r.horizontal.parse::()?; + let height = r.vertical.parse::()?; + Ok(Resolution { + width, + height, + ..Default::default() + }) +} + +pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + std::process::Command::new("xrandr") + .args(vec![ + "--output", + name, + "--mode", + &format!("{}x{}", width, height), + ]) + .spawn()?; + Ok(()) +} From 5b8e51d6b981e1b0cad80edcb56ab1cc5d6a8fb3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 12 Feb 2023 16:09:41 +0800 Subject: [PATCH 197/202] mac resolution Signed-off-by: 21pages --- src/platform/macos.mm | 111 ++++++++++++++++++++++++++++++++++++++++++ src/platform/macos.rs | 73 ++++++++++++++++++++++++++- 2 files changed, 183 insertions(+), 1 deletion(-) diff --git a/src/platform/macos.mm b/src/platform/macos.mm index 789404cb6..443351469 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -40,3 +40,114 @@ extern "C" float BackingScaleFactor() { if (s) return [s backingScaleFactor]; return 1; } + +// https://github.com/jhford/screenresolution/blob/master/cg_utils.c +// https://github.com/jdoupe/screenres/blob/master/setgetscreen.m + +extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) { + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + return false; + } + *numModes = CFArrayGetCount(allModes); + CFRelease(allModes); + return true; +} + +extern "C" bool MacGetModes(CGDirectDisplayID display, uint32_t *widths, uint32_t *heights, uint32_t max, uint32_t *numModes) { + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + return false; + } + *numModes = CFArrayGetCount(allModes); + for (int i = 0; i < *numModes && i < max; i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + widths[i] = (uint32_t)CGDisplayModeGetWidth(mode); + heights[i] = (uint32_t)CGDisplayModeGetHeight(mode); + } + CFRelease(allModes); + return true; +} + +extern "C" bool MacGetMode(CGDirectDisplayID display, uint32_t *width, uint32_t *height) { + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display); + if (mode == NULL) { + return false; + } + *width = (uint32_t)CGDisplayModeGetWidth(mode); + *height = (uint32_t)CGDisplayModeGetHeight(mode); + CGDisplayModeRelease(mode); + return true; +} + +size_t bitDepth(CGDisplayModeRef mode) { + size_t depth = 0; + CFStringRef pixelEncoding = CGDisplayModeCopyPixelEncoding(mode); + // my numerical representation for kIO16BitFloatPixels and kIO32bitFloatPixels + // are made up and possibly non-sensical + if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO32BitFloatPixels), kCFCompareCaseInsensitive)) { + depth = 96; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO64BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 64; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO16BitFloatPixels), kCFCompareCaseInsensitive)) { + depth = 48; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 32; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO30BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 30; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 16; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive)) { + depth = 8; + } + CFRelease(pixelEncoding); + return depth; +} + +bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) { + CGError rc; + CGDisplayConfigRef config; + rc = CGBeginDisplayConfiguration(&config); + if (rc != kCGErrorSuccess) { + return false; + } + rc = CGConfigureDisplayWithDisplayMode(config, display, mode, NULL); + if (rc != kCGErrorSuccess) { + return false; + } + rc = CGCompleteDisplayConfiguration(config, kCGConfigureForSession); + if (rc != kCGErrorSuccess) { + return false; + } + return true; +} + + +extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t height) +{ + bool ret = false; + CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display); + if (currentMode == NULL) { + return ret; + } + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + CGDisplayModeRelease(currentMode); + return ret; + } + int numModes = CFArrayGetCount(allModes); + CGDisplayModeRef bestMode = NULL; + for (int i = 0; i < numModes; i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + if (width == CGDisplayModeGetWidth(mode) && + height == CGDisplayModeGetHeight(mode) && + bitDepth(currentMode) == bitDepth(mode) && + CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode)) { + ret = setDisplayToMode(display, mode); + break; + } + } + CGDisplayModeRelease(currentMode); + CFRelease(allModes); + return ret; +} \ No newline at end of file diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 3e19cca28..025274840 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -17,7 +17,7 @@ use core_graphics::{ display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo}, window::{kCGWindowName, kCGWindowOwnerPID}, }; -use hbb_common::{allow_err, bail, log}; +use hbb_common::{allow_err, anyhow::anyhow, bail, log, message_proto::Resolution}; use include_dir::{include_dir, Dir}; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; @@ -34,6 +34,16 @@ extern "C" { static kAXTrustedCheckOptionPrompt: CFStringRef; fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL; fn InputMonitoringAuthStatus(_: BOOL) -> BOOL; + fn MacGetModeNum(display: u32, numModes: *mut u32) -> BOOL; + fn MacGetModes( + display: u32, + widths: *mut u32, + heights: *mut u32, + max: u32, + numModes: *mut u32, + ) -> BOOL; + fn MacGetMode(display: u32, width: *mut u32, height: *mut u32) -> BOOL; + fn MacSetMode(display: u32, width: u32, height: u32) -> BOOL; } pub fn is_process_trusted(prompt: bool) -> bool { @@ -594,3 +604,64 @@ pub fn handle_application_should_open_untitled_file() { } } } + +pub fn resolutions(name: &str) -> Vec { + let mut v = vec![]; + if let Ok(display) = name.parse::() { + let mut num = 0; + unsafe { + if YES == MacGetModeNum(display, &mut num) { + let (mut widths, mut heights) = (vec![0; num as _], vec![0; num as _]); + let mut realNum = 0; + if YES + == MacGetModes( + display, + widths.as_mut_ptr(), + heights.as_mut_ptr(), + num, + &mut realNum, + ) + { + if realNum <= num { + for i in 0..realNum { + let resolution = Resolution { + width: widths[i as usize] as _, + height: heights[i as usize] as _, + ..Default::default() + }; + if !v.contains(&resolution) { + v.push(resolution); + } + } + } + } + } + } + } + v +} + +pub fn current_resolution(name: &str) -> ResultType { + let display = name.parse::().map_err(|e| anyhow!(e))?; + unsafe { + let (mut width, mut height) = (0, 0); + if NO == MacGetMode(display, &mut width, &mut height) { + bail!("MacGetMode failed"); + } + Ok(Resolution { + width: width as _, + height: height as _, + ..Default::default() + }) + } +} + +pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + let display = name.parse::().map_err(|e| anyhow!(e))?; + unsafe { + if NO == MacSetMode(display, width as _, height as _) { + bail!("MacSetMode failed"); + } + } + Ok(()) +} From 4338451f6f7f64a9f84c38d690c11b1b78b44e7e Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 23 Feb 2023 14:30:29 +0800 Subject: [PATCH 198/202] refactor remote menubar with MenuBar for submenu Signed-off-by: 21pages --- flutter/lib/common.dart | 16 + .../desktop/pages/desktop_setting_page.dart | 34 +- .../lib/desktop/widgets/remote_menubar.dart | 2791 +++++++++-------- src/lang/ca.rs | 2 + src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 2 + src/lang/de.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 2 + src/lang/fa.rs | 2 + src/lang/fr.rs | 2 + src/lang/gr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/kz.rs | 2 + src/lang/nl.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ro.rs | 2 + src/lang/ru.rs | 2 + src/lang/sk.rs | 2 + src/lang/sl.rs | 2 + src/lang/sq.rs | 2 + src/lang/sr.rs | 2 + src/lang/sv.rs | 2 + src/lang/template.rs | 2 + src/lang/th.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/ua.rs | 2 + src/lang/vn.rs | 2 + 36 files changed, 1582 insertions(+), 1325 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6d3e4c3b7..ddd9ea1ac 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1814,3 +1814,19 @@ class DraggableNeverScrollableScrollPhysics extends ScrollPhysics { @override bool get allowImplicitScrolling => false; } + +Widget futureBuilder( + {required Future? future, required Widget Function(dynamic data) hasData}) { + return FutureBuilder( + future: future, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return hasData(snapshot.data!); + } else { + if (snapshot.hasError) { + debugPrint(snapshot.error.toString()); + } + return Container(); + } + }); +} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 971c713ce..ffe707cf0 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -319,7 +319,7 @@ class _GeneralState extends State<_General> { bind.mainSetOption(key: 'audio-input', value: device); } - return _futureBuilder(future: () async { + return futureBuilder(future: () async { List devices = (await bind.mainGetSoundInputs()).toList(); if (Platform.isWindows) { devices.insert(0, 'System Sound'); @@ -346,7 +346,7 @@ class _GeneralState extends State<_General> { } Widget record(BuildContext context) { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { String customDirectory = await bind.mainGetOption(key: 'video-save-directory'); String defaultDirectory = await bind.mainDefaultVideoSaveDirectory(); @@ -399,7 +399,7 @@ class _GeneralState extends State<_General> { } Widget language() { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { String langs = await bind.mainGetLangs(); String lang = bind.mainGetLocalOption(key: kCommConfKeyLang); return {'langs': langs, 'lang': lang}; @@ -487,7 +487,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget _permissions(context, bool stopService) { bool enabled = !locked; - return _futureBuilder(future: () async { + return futureBuilder(future: () async { return await bind.mainGetOption(key: 'access-mode'); }(), hasData: (data) { String accessMode = data! as String; @@ -744,7 +744,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { return [ _OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server', update: update, enabled: !locked), - _futureBuilder( + futureBuilder( future: () async { String enabled = await bind.mainGetOption(key: 'direct-server'); String port = await bind.mainGetOption(key: 'direct-access-port'); @@ -805,7 +805,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget whitelist() { bool enabled = !locked; - return _futureBuilder(future: () async { + return futureBuilder(future: () async { return await bind.mainGetOption(key: 'whitelist'); }(), hasData: (data) { RxBool hasWhitelist = (data as String).isNotEmpty.obs; @@ -931,7 +931,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } server(bool enabled) { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { return await bind.mainGetOptions(); }(), hasData: (data) { // Setting page is not modal, oldOptions should only be used when getting options, never when setting. @@ -1366,7 +1366,7 @@ class _About extends StatefulWidget { class _AboutState extends State<_About> { @override Widget build(BuildContext context) { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { final license = await bind.mainGetLicense(); final version = await bind.mainGetVersion(); final buildDate = await bind.mainGetBuildDate(); @@ -1500,7 +1500,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key, bool enabled = true, Icon? checkedIcon, bool? fakeValue}) { - return _futureBuilder( + return futureBuilder( future: bind.mainGetOption(key: key), hasData: (data) { bool value = option2bool(key, data.toString()); @@ -1633,22 +1633,6 @@ Widget _SubLabeledWidget(BuildContext context, String label, Widget child, ).marginOnly(left: _kContentHSubMargin); } -Widget _futureBuilder( - {required Future? future, required Widget Function(dynamic data) hasData}) { - return FutureBuilder( - future: future, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return hasData(snapshot.data!); - } else { - if (snapshot.hasError) { - debugPrint(snapshot.error.toString()); - } - return Container(); - } - }); -} - Widget _lock( bool locked, String label, diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 45857aa45..993d02683 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1,11 +1,9 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:math' as math; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_hbb/desktop/widgets/menu_button.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/consts.dart'; @@ -23,7 +21,6 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import './popup_menu.dart'; -import './material_mod_popup_menu.dart' as mod_menu; import './kb_layout_type_chooser.dart'; class MenubarState { @@ -102,6 +99,11 @@ class _MenubarTheme { // kMinInteractiveDimension static const double height = 20.0; static const double dividerHeight = 12.0; + + static const double buttonSize = 32; + static const double buttonHMargin = 3; + static const double buttonVMargin = 6; + static const double iconRadius = 8; } typedef DismissFunc = void Function(); @@ -280,7 +282,7 @@ class RemoteMenubar extends StatefulWidget { final Function(Function(bool)) onEnterOrLeaveImageSetter; final Function() onEnterOrLeaveImageCleaner; - const RemoteMenubar({ + RemoteMenubar({ Key? key, required this.id, required this.ffi, @@ -296,7 +298,6 @@ class RemoteMenubar extends StatefulWidget { class _RemoteMenubarState extends State { late Debouncer _debouncerHide; bool _isCursorOverImage = false; - window_size.Screen? _screen; final _fractionX = 0.5.obs; final _dragging = false.obs; @@ -347,7 +348,6 @@ class _RemoteMenubarState extends State { @override Widget build(BuildContext context) { // No need to use future builder here. - _updateScreen(); return Align( alignment: Alignment.topCenter, child: Obx(() => show.value @@ -375,6 +375,577 @@ class _RemoteMenubarState extends State { }); } + Widget _buildMenubar(BuildContext context) { + final List menubarItems = []; + if (!isWebDesktop) { + menubarItems.add(_PinMenu(state: widget.state)); + menubarItems.add( + _FullscreenMenu(state: widget.state, setFullscreen: _setFullscreen)); + menubarItems.add(_MobileActionMenu(ffi: widget.ffi)); + } + menubarItems.add(_MonitorMenu(id: widget.id, ffi: widget.ffi)); + menubarItems + .add(_ControlMenu(id: widget.id, ffi: widget.ffi, state: widget.state)); + menubarItems.add(_DisplayMenu( + id: widget.id, + ffi: widget.ffi, + state: widget.state, + setFullscreen: _setFullscreen, + )); + menubarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); + if (!isWeb) { + menubarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); + menubarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); + } + menubarItems.add(_RecordMenu()); + menubarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi)); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MenuBar( + children: [ + SizedBox(width: _MenubarTheme.buttonHMargin), + ...menubarItems, + SizedBox(width: _MenubarTheme.buttonHMargin) + ], + )), + ), + _buildDraggableShowHide(context), + ], + ); + } +} + +class _PinMenu extends StatelessWidget { + final MenubarState state; + const _PinMenu({Key? key, required this.state}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx( + () => _IconMenuButton( + assetName: state.pin ? "assets/pinned.svg" : "assets/unpinned.svg", + tooltip: state.pin ? 'Unpin menubar' : 'Pin menubar', + onPressed: state.switchPin, + color: state.pin ? _MenubarTheme.blueColor : Colors.grey[800]!, + hoverColor: + state.pin ? _MenubarTheme.hoverBlueColor : Colors.grey[850]!, + ), + ); + } +} + +class _FullscreenMenu extends StatelessWidget { + final MenubarState state; + final Function(bool) setFullscreen; + bool get isFullscreen => stateGlobal.fullscreen; + const _FullscreenMenu( + {Key? key, required this.state, required this.setFullscreen}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _IconMenuButton( + assetName: + isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", + tooltip: isFullscreen ? 'Exit Fullscreen' : 'Fullscreen', + onPressed: () => setFullscreen(!isFullscreen), + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + ); + } +} + +class _MobileActionMenu extends StatelessWidget { + final FFI ffi; + const _MobileActionMenu({Key? key, required this.ffi}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (!ffi.ffiModel.isPeerAndroid) return Offstage(); + return _IconMenuButton( + assetName: 'assets/actions_mobile.svg', + tooltip: 'Mobile Actions', + onPressed: () => ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi), + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + ); + } +} + +class _MonitorMenu extends StatelessWidget { + final String id; + final FFI ffi; + const _MonitorMenu({Key? key, required this.id, required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + if (stateGlobal.displaysCount.value < 2) return Offstage(); + return _IconSubmenuButton( + icon: icon(), + ffi: ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuStyle: MenuStyle( + padding: + MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))), + menuChildren: [Row(children: displays(context))]); + } + + icon() { + final pi = ffi.ffiModel.pi; + return Stack( + alignment: Alignment.center, + children: [ + SvgPicture.asset( + "assets/display.svg", + color: Colors.white, + ), + Padding( + padding: const EdgeInsets.only(bottom: 3.9), + child: Obx(() { + RxInt display = CurrentDisplayState.find(id); + return Text( + '${display.value + 1}/${pi.displays.length}', + style: const TextStyle(color: Colors.white, fontSize: 8), + ); + }), + ) + ], + ); + } + + List displays(BuildContext context) { + final List rowChildren = []; + final pi = ffi.ffiModel.pi; + for (int i = 0; i < pi.displays.length; i++) { + rowChildren.add(_IconMenuButton( + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + tooltip: "", + hMargin: 6, + vMargin: 12, + icon: Container( + alignment: AlignmentDirectional.center, + constraints: const BoxConstraints(minHeight: _MenubarTheme.height), + child: Stack( + alignment: Alignment.center, + children: [ + SvgPicture.asset( + "assets/display.svg", + color: Colors.white, + ), + Padding( + padding: const EdgeInsets.only(bottom: 3.5 /*2.5*/), + child: Text( + (i + 1).toString(), + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ) + ], + ), + ), + onPressed: () { + _menuDismissCallback(ffi); + RxInt display = CurrentDisplayState.find(id); + if (display.value != i) { + bind.sessionSwitchDisplay(id: id, value: i); + } + }, + )); + } + return rowChildren; + } +} + +class _ControlMenu extends StatelessWidget { + final String id; + final FFI ffi; + final MenubarState state; + _ControlMenu( + {Key? key, required this.id, required this.ffi, required this.state}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _IconSubmenuButton( + svg: "assets/actions.svg", + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + ffi: ffi, + menuChildren: [ + osPassword(), + transferFile(context), + tcpTunneling(context), + note(), + Divider(), + ctrlAltDel(), + restart(), + blockUserInput(), + switchSides(), + refresh(), + ]); + } + + osPassword() { + return _MenuItemButton( + child: Text(translate('OS Password')), + trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)), + ffi: ffi, + onPressed: () => _showSetOSPassword(id, false, ffi.dialogManager)); + } + + _showSetOSPassword( + String id, bool login, OverlayDialogManager dialogManager) async { + final controller = TextEditingController(); + var password = + await bind.sessionGetOption(id: id, arg: 'os-password') ?? ''; + var autoLogin = + await bind.sessionGetOption(id: id, arg: 'auto-login') != ''; + controller.text = password; + dialogManager.show((setState, close) { + submit() { + var text = controller.text.trim(); + bind.sessionPeerOption(id: id, name: 'os-password', value: text); + bind.sessionPeerOption( + id: id, name: 'auto-login', value: autoLogin ? 'Y' : ''); + if (text != '' && login) { + bind.sessionInputOsPassword(id: id, value: text); + } + close(); + } + + return CustomAlertDialog( + title: Text(translate('OS Password')), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + PasswordWidget(controller: controller), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate('Auto Login'), + ), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = v); + }, + ), + ]), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + transferFile(BuildContext context) { + return _MenuItemButton( + child: Text(translate('Transfer File')), + ffi: ffi, + onPressed: () => connect(context, id, isFileTransfer: true)); + } + + tcpTunneling(BuildContext context) { + return _MenuItemButton( + child: Text(translate('TCP Tunneling')), + ffi: ffi, + onPressed: () => connect(context, id, isTcpTunneling: true)); + } + + note() { + final auditServer = bind.sessionGetAuditServerSync(id: id, typ: "conn"); + final visible = auditServer.isNotEmpty; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Note')), + ffi: ffi, + onPressed: () => _showAuditDialog(id, ffi.dialogManager), + ); + } + + _showAuditDialog(String id, dialogManager) async { + final controller = TextEditingController(); + dialogManager.show((setState, close) { + submit() { + var text = controller.text.trim(); + if (text != '') { + bind.sessionSendNote(id: id, note: text); + } + close(); + } + + late final focusNode = FocusNode( + onKey: (FocusNode node, RawKeyEvent evt) { + if (evt.logicalKey.keyLabel == 'Enter') { + if (evt is RawKeyDownEvent) { + int pos = controller.selection.base.offset; + controller.text = + '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; + controller.selection = + TextSelection.fromPosition(TextPosition(offset: pos + 1)); + } + return KeyEventResult.handled; + } + if (evt.logicalKey.keyLabel == 'Esc') { + if (evt is RawKeyDownEvent) { + close(); + } + return KeyEventResult.handled; + } else { + return KeyEventResult.ignored; + } + }, + ); + + return CustomAlertDialog( + title: Text(translate('Note')), + content: SizedBox( + width: 250, + height: 120, + child: TextField( + autofocus: true, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.newline, + decoration: const InputDecoration.collapsed( + hintText: 'input note here', + ), + maxLines: null, + maxLength: 256, + controller: controller, + focusNode: focusNode, + )), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit) + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + ctrlAltDel() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = perms['keyboard'] != false && + (pi.platform == kPeerPlatformLinux || pi.sasEnabled); + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text('${translate("Insert")} Ctrl + Alt + Del'), + ffi: ffi, + onPressed: () => bind.sessionCtrlAltDel(id: id)); + } + + restart() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = perms['restart'] != false && + (pi.platform == kPeerPlatformLinux || + pi.platform == kPeerPlatformWindows || + pi.platform == kPeerPlatformMacOS); + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Restart Remote Device')), + ffi: ffi, + onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager)); + } + + blockUserInput() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = + perms['keyboard'] != false && pi.platform == kPeerPlatformWindows; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Obx(() => Text(translate( + '${BlockInputState.find(id).value ? 'Unb' : 'B'}lock user input'))), + ffi: ffi, + onPressed: () { + RxBool blockInput = BlockInputState.find(id); + bind.sessionToggleOption( + id: id, value: '${blockInput.value ? 'un' : ''}block-input'); + blockInput.value = !blockInput.value; + }); + } + + switchSides() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = perms['keyboard'] != false && + pi.platform != kPeerPlatformAndroid && + pi.platform != kPeerPlatformMacOS && + version_cmp(pi.version, '1.2.0') >= 0; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Switch Sides')), + ffi: ffi, + onPressed: () => _showConfirmSwitchSidesDialog(id, ffi.dialogManager)); + } + + void _showConfirmSwitchSidesDialog( + String id, OverlayDialogManager dialogManager) async { + dialogManager.show((setState, close) { + submit() async { + await bind.sessionSwitchSides(id: id); + closeConnection(id: id); + } + + return CustomAlertDialog( + content: msgboxContent('info', 'Switch Sides', + 'Please confirm if you want to share your desktop?'), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + refresh() { + final pi = ffi.ffiModel.pi; + final visible = pi.version.isNotEmpty; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Refresh')), + ffi: ffi, + onPressed: () => bind.sessionRefresh(id: id)); + } +} + +class _DisplayMenu extends StatefulWidget { + final String id; + final FFI ffi; + final MenubarState state; + final Function(bool) setFullscreen; + _DisplayMenu( + {Key? key, + required this.id, + required this.ffi, + required this.state, + required this.setFullscreen}) + : super(key: key); + + @override + State<_DisplayMenu> createState() => _DisplayMenuState(); +} + +class _DisplayMenuState extends State<_DisplayMenu> { + window_size.Screen? _screen; + + bool get isFullscreen => stateGlobal.fullscreen; + + int get windowId => stateGlobal.windowId; + + Map get perms => widget.ffi.ffiModel.permissions; + + PeerInfo get pi => widget.ffi.ffiModel.pi; + + @override + Widget build(BuildContext context) { + _updateScreen(); + return _IconSubmenuButton( + svg: "assets/display.svg", + ffi: widget.ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuChildren: [ + adjustWindow(), + viewStyle(), + scrollStyle(), + imageQuality(), + codec(), + resolutions(), + Divider(), + showRemoteCursor(), + zoomCursor(), + showQualityMonitor(), + mute(), + fileCopyAndPaste(), + disableClipboard(), + lockAfterSessionEnd(), + privacyMode(), + ]); + } + + adjustWindow() { + final visible = _isWindowCanBeAdjusted(); + if (!visible) return Offstage(); + return Column( + children: [ + _MenuItemButton( + child: Text(translate('Adjust Window')), + onPressed: _doAdjustWindow, + ffi: widget.ffi), + Divider(), + ], + ); + } + + _doAdjustWindow() async { + await _updateScreen(); + if (_screen != null) { + widget.setFullscreen(false); + double scale = _screen!.scaleFactor; + final wndRect = await WindowController.fromWindowId(windowId).getFrame(); + final mediaSize = MediaQueryData.fromWindow(ui.window).size; + // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect. + // https://stackoverflow.com/a/7561083 + double magicWidth = + wndRect.right - wndRect.left - mediaSize.width * scale; + double magicHeight = + wndRect.bottom - wndRect.top - mediaSize.height * scale; + + final canvasModel = widget.ffi.canvasModel; + final width = (canvasModel.getDisplayWidth() * canvasModel.scale + + canvasModel.windowBorderWidth * 2) * + scale + + magicWidth; + final height = (canvasModel.getDisplayHeight() * canvasModel.scale + + canvasModel.tabBarHeight + + canvasModel.windowBorderWidth * 2) * + scale + + magicHeight; + double left = wndRect.left + (wndRect.width - width) / 2; + double top = wndRect.top + (wndRect.height - height) / 2; + + Rect frameRect = _screen!.frame; + if (!isFullscreen) { + frameRect = _screen!.visibleFrame; + } + if (left < frameRect.left) { + left = frameRect.left; + } + if (top < frameRect.top) { + top = frameRect.top; + } + if ((left + width) > frameRect.right) { + left = frameRect.right - width; + } + if ((top + height) > frameRect.bottom) { + top = frameRect.bottom - height; + } + await WindowController.fromWindowId(windowId) + .setFrame(Rect.fromLTWH(left, top, width, height)); + } + } + _updateScreen() async { final v = await rustDeskWinManager.call( WindowType.Main, kWindowGetWindowInfo, ''); @@ -395,638 +966,11 @@ class _RemoteMenubarState extends State { } } - Widget _buildPointerTrackWidget(Widget child) { - return Listener( - onPointerHover: (PointerHoverEvent e) => - widget.ffi.inputModel.lastMousePos = e.position, - child: MouseRegion( - child: child, - ), - ); - } - - _menuDismissCallback() => widget.ffi.inputModel.refreshMousePos(); - - Widget _buildMenubar(BuildContext context) { - final List menubarItems = []; - if (!isWebDesktop) { - menubarItems.add(_buildPinMenubar(context)); - menubarItems.add(_buildFullscreen(context)); - if (widget.ffi.ffiModel.isPeerAndroid) { - menubarItems.add(MenuButton( - tooltip: translate('Mobile Actions'), - child: SvgPicture.asset( - "assets/actions_mobile.svg", - color: Colors.white, - ), - onPressed: () { - widget.ffi.dialogManager - .toggleMobileActionsOverlay(ffi: widget.ffi); - }, - color: _MenubarTheme.blueColor, - hoverColor: _MenubarTheme.hoverBlueColor, - )); - } + _isWindowCanBeAdjusted() { + if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) { + return false; } - menubarItems.add(_buildMonitor(context)); - menubarItems.add(_buildControl(context)); - menubarItems.add(_buildDisplay(context)); - menubarItems.add(_buildKeyboard(context)); - if (!isWeb) { - menubarItems.add(_buildChat(context)); - menubarItems.add(_buildVoiceCall(context)); - } - menubarItems.add(_buildRecording(context)); - menubarItems.add(_buildClose(context)); - return PopupMenuTheme( - data: const PopupMenuThemeData( - textStyle: TextStyle(color: _MenubarTheme.blueColor)), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(10), - ), - ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: 3), - ...menubarItems, - SizedBox(width: 3) - ], - ), - ), - ), - _buildDraggableShowHide(context), - ], - ), - ); - } - - Widget _buildPinMenubar(BuildContext context) { - return Obx( - () => MenuButton( - tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), - onPressed: () { - widget.state.switchPin(); - }, - child: SvgPicture.asset( - pin ? "assets/pinned.svg" : "assets/unpinned.svg", - color: Colors.white, - ), - color: pin ? _MenubarTheme.blueColor : Colors.grey[800]!, - hoverColor: pin ? _MenubarTheme.hoverBlueColor : Colors.grey[850]!, - ), - ); - } - - Widget _buildFullscreen(BuildContext context) { - return MenuButton( - tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), - onPressed: () { - _setFullscreen(!isFullscreen); - }, - child: SvgPicture.asset( - isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", - color: Colors.white, - ), - color: _MenubarTheme.blueColor, - hoverColor: _MenubarTheme.hoverBlueColor, - ); - } - - Widget _buildMonitor(BuildContext context) { - final pi = widget.ffi.ffiModel.pi; - final monitor = mod_menu.PopupMenuButton( - tooltip: translate('Select Monitor'), - position: mod_menu.PopupMenuPosition.under, - icon: Stack( - alignment: Alignment.center, - children: [ - SvgPicture.asset( - "assets/display.svg", - color: Colors.white, - ), - Padding( - padding: const EdgeInsets.only(bottom: 3.9), - child: Obx(() { - RxInt display = CurrentDisplayState.find(widget.id); - return Text( - '${display.value + 1}/${pi.displays.length}', - style: const TextStyle(color: Colors.white, fontSize: 8), - ); - }), - ) - ], - ), - itemBuilder: (BuildContext context) { - final List rowChildren = []; - for (int i = 0; i < pi.displays.length; i++) { - rowChildren.add(MenuButton( - color: _MenubarTheme.blueColor, - hoverColor: _MenubarTheme.hoverBlueColor, - child: Container( - alignment: AlignmentDirectional.center, - constraints: - const BoxConstraints(minHeight: _MenubarTheme.height), - child: Stack( - alignment: Alignment.center, - children: [ - SvgPicture.asset( - "assets/display.svg", - color: Colors.white, - ), - Padding( - padding: const EdgeInsets.only(bottom: 2.5), - child: Text( - (i + 1).toString(), - style: TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ) - ], - ), - ), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - _menuDismissCallback(); - } - RxInt display = CurrentDisplayState.find(widget.id); - if (display.value != i) { - bind.sessionSwitchDisplay(id: widget.id, value: i); - } - }, - )); - } - return >[ - mod_menu.PopupMenuItem( - height: _MenubarTheme.height, - padding: EdgeInsets.zero, - child: _buildPointerTrackWidget( - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: rowChildren, - ), - ), - ) - ]; - }, - ); - - return Obx(() => Offstage( - offstage: stateGlobal.displaysCount.value < 2, - child: monitor, - )); - } - - Widget _buildControl(BuildContext context) { - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: SvgPicture.asset( - "assets/actions.svg", - color: Colors.white, - ), - tooltip: translate('Control Actions'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getControlMenu(context) - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.blueColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - } - - Widget _buildDisplay(BuildContext context) { - return FutureBuilder(future: () async { - widget.state.viewStyle.value = - await bind.sessionGetViewStyle(id: widget.id) ?? ''; - final supportedHwcodec = - await bind.sessionSupportedHwcodec(id: widget.id); - return {'supportedHwcodec': supportedHwcodec}; - }(), builder: (context, snapshot) { - if (snapshot.hasData) { - return Obx(() { - final remoteCount = RemoteCountState.find().value; - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: SvgPicture.asset( - "assets/display.svg", - color: Colors.white, - ), - tooltip: translate('Display Settings'), - position: mod_menu.PopupMenuPosition.under, - menuWrapper: _buildPointerTrackWidget, - itemBuilder: (BuildContext context) => - _getDisplayMenu(snapshot.data!, remoteCount) - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.blueColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - }); - } else { - return const Offstage(); - } - }); - } - - Widget _buildKeyboard(BuildContext context) { - // Do not support peer 1.1.9. - if (stateGlobal.grabKeyboard) { - bind.sessionSetKeyboardMode(id: widget.id, value: 'map'); - return Offstage(); - } - - FfiModel ffiModel = Provider.of(context); - if (ffiModel.permissions['keyboard'] == false) { - return Offstage(); - } - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: SvgPicture.asset( - "assets/keyboard.svg", - color: Colors.white, - ), - tooltip: translate('Keyboard Settings'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getKeyboardMenu() - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.blueColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - } - - Widget _buildRecording(BuildContext context) { - return Consumer(builder: ((context, value, child) { - if (value.permissions['recording'] != false) { - return Consumer( - builder: (context, value, child) => MenuButton( - tooltip: value.start - ? translate('Stop session recording') - : translate('Start session recording'), - onPressed: () => value.toggle(), - child: SvgPicture.asset( - "assets/rec.svg", - color: Colors.white, - ), - color: - value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor, - hoverColor: value.start - ? _MenubarTheme.hoverRedColor - : _MenubarTheme.hoverBlueColor, - ), - ); - } else { - return Offstage(); - } - })); - } - - Widget _buildClose(BuildContext context) { - return MenuButton( - tooltip: translate('Close'), - onPressed: () { - clientClose(widget.id, widget.ffi.dialogManager); - }, - child: SvgPicture.asset( - "assets/close.svg", - color: Colors.white, - ), - color: _MenubarTheme.redColor, - hoverColor: _MenubarTheme.hoverRedColor, - ); - } - - final _chatButtonKey = GlobalKey(); - Widget _buildChat(BuildContext context) { - FfiModel ffiModel = Provider.of(context); - return mod_menu.PopupMenuButton( - key: _chatButtonKey, - padding: EdgeInsets.zero, - icon: SvgPicture.asset( - "assets/chat.svg", - color: Colors.white, - ), - tooltip: translate('Chat'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getChatMenu(context) - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.blueColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - } - - Widget _getVoiceCallIcon() { - switch (widget.ffi.chatModel.voiceCallStatus.value) { - case VoiceCallStatus.waitingForResponse: - return SvgPicture.asset( - "assets/call_wait.svg", - color: Colors.white, - ); - - case VoiceCallStatus.connected: - return SvgPicture.asset( - "assets/call_end.svg", - color: Colors.white, - ); - default: - return const Offstage(); - } - } - - String? _getVoiceCallTooltip() { - switch (widget.ffi.chatModel.voiceCallStatus.value) { - case VoiceCallStatus.waitingForResponse: - return "Waiting"; - case VoiceCallStatus.connected: - return "Disconnect"; - default: - return null; - } - } - - Widget _buildVoiceCall(BuildContext context) { - return Obx( - () { - final tooltipText = _getVoiceCallTooltip(); - return tooltipText == null - ? const Offstage() - : MenuButton( - child: _getVoiceCallIcon(), - tooltip: translate(tooltipText), - onPressed: () => bind.sessionCloseVoiceCall(id: widget.id), - color: _MenubarTheme.redColor, - hoverColor: _MenubarTheme.hoverRedColor, - ); - }, - ); - } - - List> _getChatMenu(BuildContext context) { - final List> chatMenu = []; - const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); - chatMenu.addAll([ - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Text chat'), - style: style, - ), - proc: () { - RenderBox? renderBox = - _chatButtonKey.currentContext?.findRenderObject() as RenderBox?; - - Offset? initPos; - if (renderBox != null) { - final pos = renderBox.localToGlobal(Offset.zero); - initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight); - } - - widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); - widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); - }, - padding: padding, - dismissOnClicked: true, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Voice call'), - style: style, - ), - proc: () { - // Request a voice call. - bind.sessionRequestVoiceCall(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - ), - ]); - return chatMenu; - } - - List> _getControlMenu(BuildContext context) { - final pi = widget.ffi.ffiModel.pi; - final perms = widget.ffi.ffiModel.permissions; - final peer_version = widget.ffi.ffiModel.pi.version; - const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); - final List> displayMenu = []; - displayMenu.addAll([ - MenuEntryButton( - childBuilder: (TextStyle? style) => Container( - alignment: AlignmentDirectional.center, - height: _MenubarTheme.height, - child: Row( - children: [ - Text( - translate('OS Password'), - style: style, - ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: 0.8, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.edit), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - _menuDismissCallback(); - } - showSetOSPassword( - widget.id, false, widget.ffi.dialogManager); - })), - )) - ], - )), - proc: () { - showSetOSPassword(widget.id, false, widget.ffi.dialogManager); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Transfer File'), - style: style, - ), - proc: () { - connect(context, widget.id, isFileTransfer: true); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('TCP Tunneling'), - style: style, - ), - padding: padding, - proc: () { - connect(context, widget.id, isTcpTunneling: true); - }, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ]); - // {handler.get_audit_server() &&

  • {translate('Note')}
  • } - final auditServer = - bind.sessionGetAuditServerSync(id: widget.id, typ: "conn"); - if (auditServer.isNotEmpty) { - displayMenu.add( - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Note'), - style: style, - ), - proc: () { - showAuditDialog(widget.id, widget.ffi.dialogManager); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ); - } - displayMenu.add(MenuEntryDivider()); - if (perms['keyboard'] != false) { - if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { - displayMenu.add(RemoteMenuEntry.insertCtrlAltDel(widget.id, padding, - dismissCallback: _menuDismissCallback)); - } - } - if (perms['restart'] != false && - (pi.platform == kPeerPlatformLinux || - pi.platform == kPeerPlatformWindows || - pi.platform == kPeerPlatformMacOS)) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Restart Remote Device'), - style: style, - ), - proc: () { - showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - - if (perms['keyboard'] != false) { - displayMenu.add(RemoteMenuEntry.insertLock(widget.id, padding, - dismissCallback: _menuDismissCallback)); - - if (pi.platform == kPeerPlatformWindows) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Obx(() => Text( - translate( - '${BlockInputState.find(widget.id).value ? 'Unb' : 'B'}lock user input'), - style: style, - )), - proc: () { - RxBool blockInput = BlockInputState.find(widget.id); - bind.sessionToggleOption( - id: widget.id, - value: '${blockInput.value ? 'un' : ''}block-input'); - blockInput.value = !blockInput.value; - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - if (pi.platform != kPeerPlatformAndroid && - pi.platform != kPeerPlatformMacOS && // unsupport yet - version_cmp(peer_version, '1.2.0') >= 0) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Switch Sides'), - style: style, - ), - proc: () => - showConfirmSwitchSidesDialog(widget.id, widget.ffi.dialogManager), - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - } - - if (pi.version.isNotEmpty) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Refresh'), - style: style, - ), - proc: () { - bind.sessionRefresh(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - - if (!isWebDesktop) { - // if (perms['keyboard'] != false && perms['clipboard'] != false) { - // displayMenu.add(MenuEntryButton( - // childBuilder: (TextStyle? style) => Text( - // translate('Paste'), - // style: style, - // ), - // proc: () { - // () async { - // ClipboardData? data = - // await Clipboard.getData(Clipboard.kTextPlain); - // if (data != null && data.text != null) { - // bind.sessionInputString(id: widget.id, value: data.text ?? ''); - // } - // }(); - // }, - // padding: padding, - // dismissOnClicked: true, - // dismissCallback: _menuDismissCallback, - // )); - // } - } - return displayMenu; - } - - bool _isWindowCanBeAdjusted(int remoteCount) { + final remoteCount = RemoteCountState.find().value; if (remoteCount != 1) { return false; } @@ -1052,312 +996,277 @@ class _RemoteMenubarState extends State { selfHeight > (requiredHeight * scale); } - List> _getDisplayMenu( - dynamic futureData, int remoteCount) { - const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0); - final peer_version = widget.ffi.ffiModel.pi.version; - final displayMenu = [ - RemoteMenuEntry.viewStyle( - widget.id, - widget.ffi, - padding, - dismissCallback: _menuDismissCallback, - rxViewStyle: widget.state.viewStyle, - ), - MenuEntryDivider(), - MenuEntryRadios( - text: translate('Image Quality'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Good image quality'), + viewStyle() { + return futureBuilder(future: () async { + final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? ''; + widget.state.viewStyle.value = viewStyle; + return viewStyle; + }(), hasData: (data) { + final groupValue = data as String; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetViewStyle(id: widget.id, value: value); + widget.state.viewStyle.value = value; + widget.ffi.canvasModel.updateViewStyle(); + } + + return Column(children: [ + _RadioMenuButton( + child: Text(translate('Scale original')), + value: kRemoteViewStyleOriginal, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('Scale adaptive')), + value: kRemoteViewStyleAdaptive, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + Divider(), + ]); + }); + } + + scrollStyle() { + final visible = widget.state.viewStyle.value == kRemoteViewStyleOriginal; + if (!visible) return Offstage(); + return futureBuilder(future: () async { + final scrollStyle = await bind.sessionGetScrollStyle(id: widget.id) ?? ''; + return scrollStyle; + }(), hasData: (data) { + final groupValue = data as String; + onChange(String? value) async { + if (value == null) return; + await bind.sessionSetScrollStyle(id: widget.id, value: value); + widget.ffi.canvasModel.updateScrollStyle(); + } + + final enabled = widget.ffi.canvasModel.imageOverflow.value; + return Column(children: [ + _RadioMenuButton( + child: Text(translate('ScrollAuto')), + value: kRemoteScrollStyleAuto, + groupValue: groupValue, + onChanged: enabled ? (value) => onChange(value) : null, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('Scrollbar')), + value: kRemoteScrollStyleBar, + groupValue: groupValue, + onChanged: enabled ? (value) => onChange(value) : null, + ffi: widget.ffi, + ), + Divider(), + ]); + }); + } + + imageQuality() { + return futureBuilder(future: () async { + final imageQuality = + await bind.sessionGetImageQuality(id: widget.id) ?? ''; + return imageQuality; + }(), hasData: (data) { + final groupValue = data as String; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetImageQuality(id: widget.id, value: value); + } + + return SubmenuButton( + child: Text(translate('Image Quality')), + menuChildren: [ + _RadioMenuButton( + child: Text(translate('Good image quality')), value: kRemoteImageQualityBest, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, ), - MenuEntryRadioOption( - text: translate('Balanced'), + _RadioMenuButton( + child: Text(translate('Balanced')), value: kRemoteImageQualityBalanced, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, ), - MenuEntryRadioOption( - text: translate('Optimize reaction time'), + _RadioMenuButton( + child: Text(translate('Optimize reaction time')), value: kRemoteImageQualityLow, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, ), - MenuEntryRadioOption( - text: translate('Custom'), + _RadioMenuButton( + child: Text(translate('Custom')), value: kRemoteImageQualityCustom, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetImageQuality(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - if (oldValue != newValue) { - await bind.sessionSetImageQuality(id: widget.id, value: newValue); - } - - double qualityInitValue = 50; - double fpsInitValue = 30; - bool qualitySet = false; - bool fpsSet = false; - setCustomValues({double? quality, double? fps}) async { - if (quality != null) { - qualitySet = true; - await bind.sessionSetCustomImageQuality( - id: widget.id, value: quality.toInt()); - } - if (fps != null) { - fpsSet = true; - await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt()); - } - if (!qualitySet) { - qualitySet = true; - await bind.sessionSetCustomImageQuality( - id: widget.id, value: qualityInitValue.toInt()); - } - if (!fpsSet) { - fpsSet = true; - await bind.sessionSetCustomFps( - id: widget.id, fps: fpsInitValue.toInt()); - } - } - - if (newValue == kRemoteImageQualityCustom) { - final btnClose = dialogButton('Close', onPressed: () async { - await setCustomValues(); - widget.ffi.dialogManager.dismissAll(); - }); - - // quality - final quality = - await bind.sessionGetCustomImageQuality(id: widget.id); - 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 > 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: [ - Slider( - value: qualitySliderValue.value, - min: qualityMinValue, - max: qualityMaxValue, - divisions: 18, - onChanged: (double value) { - qualitySliderValue.value = value; - debouncerQuality.value = value; - }, - ), - SizedBox( - width: 40, - child: Text( - '${qualitySliderValue.value.round()}%', - style: const TextStyle(fontSize: 15), - )), - SizedBox( - width: 50, - child: Text( - translate('Bitrate'), - style: const TextStyle(fontSize: 15), - )) - ], - )); - // fps - final fpsOption = - await bind.sessionGetOption(id: widget.id, arg: 'custom-fps'); - fpsInitValue = - fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30; - if (fpsInitValue < 10 || 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(widget.id).direct.value == - ConnectionType.strDirect; - } catch (_) {} - final fpsSlider = Offstage( - offstage: - (await bind.mainIsUsingPublicServer() && direct != true) || - version_cmp(peer_version, '1.2.0') < 0, - child: Row( - children: [ - Obx((() => Slider( - value: fpsSliderValue.value, - min: 10, - max: 120, - divisions: 22, - onChanged: (double value) { - fpsSliderValue.value = value; - debouncerFps.value = value; - }, - ))), - SizedBox( - width: 40, - child: Obx(() => Text( - '${fpsSliderValue.value.round()}', - style: const TextStyle(fontSize: 15), - ))), - SizedBox( - width: 50, - child: Text( - translate('FPS'), - style: const TextStyle(fontSize: 15), - )) - ], - ), - ); - - final content = Column( - children: [qualitySlider, fpsSlider], - ); - msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', - content, [btnClose]); - } - }, - padding: padding, - ), - MenuEntryDivider(), - ]; - - if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { - displayMenu.insert( - 2, - MenuEntryRadios( - text: translate('Scroll Style'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('ScrollAuto'), - value: kRemoteScrollStyleAuto, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - enabled: widget.ffi.canvasModel.imageOverflow, - ), - MenuEntryRadioOption( - text: translate('Scrollbar'), - value: kRemoteScrollStyleBar, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - enabled: widget.ffi.canvasModel.imageOverflow, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetScrollStyle(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetScrollStyle(id: widget.id, value: newValue); - widget.ffi.canvasModel.updateScrollStyle(); + groupValue: groupValue, + onChanged: (value) { + onChanged(value); + _customImageQualityDialog(); }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - displayMenu.insert(3, MenuEntryDivider()); - - if (_isWindowCanBeAdjusted(remoteCount)) { - displayMenu.insert( - 0, - MenuEntryDivider(), - ); - displayMenu.insert( - 0, - MenuEntryButton( - childBuilder: (TextStyle? style) => Container( - child: Text( - translate('Adjust Window'), - style: style, - )), - proc: () { - () async { - await _updateScreen(); - if (_screen != null) { - _setFullscreen(false); - double scale = _screen!.scaleFactor; - final wndRect = - await WindowController.fromWindowId(windowId).getFrame(); - final mediaSize = MediaQueryData.fromWindow(ui.window).size; - // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect. - // https://stackoverflow.com/a/7561083 - double magicWidth = - wndRect.right - wndRect.left - mediaSize.width * scale; - double magicHeight = - wndRect.bottom - wndRect.top - mediaSize.height * scale; - - final canvasModel = widget.ffi.canvasModel; - final width = - (canvasModel.getDisplayWidth() * canvasModel.scale + - canvasModel.windowBorderWidth * 2) * - scale + - magicWidth; - final height = - (canvasModel.getDisplayHeight() * canvasModel.scale + - canvasModel.tabBarHeight + - canvasModel.windowBorderWidth * 2) * - scale + - magicHeight; - double left = wndRect.left + (wndRect.width - width) / 2; - double top = wndRect.top + (wndRect.height - height) / 2; - - Rect frameRect = _screen!.frame; - if (!isFullscreen) { - frameRect = _screen!.visibleFrame; - } - if (left < frameRect.left) { - left = frameRect.left; - } - if (top < frameRect.top) { - top = frameRect.top; - } - if ((left + width) > frameRect.right) { - left = frameRect.right - width; - } - if ((top + height) > frameRect.bottom) { - top = frameRect.bottom - height; - } - await WindowController.fromWindowId(windowId) - .setFrame(Rect.fromLTWH(left, top, width, height)); - } - }(); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, + ffi: widget.ffi, ), - ); + ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList(), + ); + }); + } + + _customImageQualityDialog() async { + double qualityInitValue = 50; + double fpsInitValue = 30; + bool qualitySet = false; + bool fpsSet = false; + setCustomValues({double? quality, double? fps}) async { + if (quality != null) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + id: widget.id, value: quality.toInt()); + } + if (fps != null) { + fpsSet = true; + await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt()); + } + if (!qualitySet) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + id: widget.id, value: qualityInitValue.toInt()); + } + if (!fpsSet) { + fpsSet = true; + await bind.sessionSetCustomFps( + id: widget.id, fps: fpsInitValue.toInt()); } } - /// Show Codec Preference - if (bind.mainHasHwcodec()) { + final btnClose = dialogButton('Close', onPressed: () async { + await setCustomValues(); + widget.ffi.dialogManager.dismissAll(); + }); + + // quality + final quality = await bind.sessionGetCustomImageQuality(id: widget.id); + 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 > 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: [ + Slider( + value: qualitySliderValue.value, + min: qualityMinValue, + max: qualityMaxValue, + divisions: 18, + onChanged: (double value) { + qualitySliderValue.value = value; + debouncerQuality.value = value; + }, + ), + SizedBox( + width: 40, + child: Text( + '${qualitySliderValue.value.round()}%', + style: const TextStyle(fontSize: 15), + )), + SizedBox( + width: 50, + child: Text( + translate('Bitrate'), + style: const TextStyle(fontSize: 15), + )) + ], + )); + // fps + final fpsOption = + await bind.sessionGetOption(id: widget.id, arg: 'custom-fps'); + fpsInitValue = fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30; + if (fpsInitValue < 10 || 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(widget.id).direct.value == + ConnectionType.strDirect; + } catch (_) {} + final fpsSlider = Offstage( + offstage: (await bind.mainIsUsingPublicServer() && direct != true) || + version_cmp(pi.version, '1.2.0') < 0, + child: Row( + children: [ + Obx((() => Slider( + value: fpsSliderValue.value, + min: 10, + max: 120, + divisions: 22, + onChanged: (double value) { + fpsSliderValue.value = value; + debouncerFps.value = value; + }, + ))), + SizedBox( + width: 40, + child: Obx(() => Text( + '${fpsSliderValue.value.round()}', + style: const TextStyle(fontSize: 15), + ))), + SizedBox( + width: 50, + child: Text( + translate('FPS'), + style: const TextStyle(fontSize: 15), + )) + ], + ), + ); + + final content = Column( + children: [qualitySlider, fpsSlider], + ); + msgBoxCommon( + widget.ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); + } + + codec() { + return futureBuilder(future: () async { + final supportedHwcodec = + await bind.sessionSupportedHwcodec(id: widget.id); + final codecPreference = + await bind.sessionGetOption(id: widget.id, arg: 'codec-preference') ?? + ''; + return { + 'supportedHwcodec': supportedHwcodec, + 'codecPreference': codecPreference + }; + }(), hasData: (data) { final List codecs = []; try { - final Map codecsJson = jsonDecode(futureData['supportedHwcodec']); + final Map codecsJson = jsonDecode(data['supportedHwcodec']); final h264 = codecsJson['h264'] ?? false; final h265 = codecsJson['h265'] ?? false; codecs.add(h264); @@ -1365,385 +1274,655 @@ class _RemoteMenubarState extends State { } catch (e) { debugPrint("Show Codec Preference err=$e"); } - if (codecs.length == 2 && (codecs[0] || codecs[1])) { - displayMenu.add(MenuEntryRadios( - text: translate('Codec Preference'), - optionsGetter: () { - final list = [ - MenuEntryRadioOption( - text: translate('Auto'), - value: 'auto', - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - MenuEntryRadioOption( - text: 'VP9', - value: 'vp9', - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ]; - if (codecs[0]) { - list.add(MenuEntryRadioOption( - text: 'H264', - value: 'h264', - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - if (codecs[1]) { - list.add(MenuEntryRadioOption( - text: 'H265', - value: 'h265', - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - return list; - }, - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetOption( - id: widget.id, arg: 'codec-preference') ?? - '', - optionSetter: (String oldValue, String newValue) async { - await bind.sessionPeerOption( - id: widget.id, name: 'codec-preference', value: newValue); - bind.sessionChangePreferCodec(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); + final visible = bind.mainHasHwcodec() && + codecs.length == 2 && + (codecs[0] || codecs[1]); + if (!visible) return Offstage(); + final groupValue = data['codecPreference'] as String; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionPeerOption( + id: widget.id, name: 'codec-preference', value: value); + bind.sessionChangePreferCodec(id: widget.id); } - } - displayMenu.add(MenuEntryDivider()); - /// Show remote cursor - if (!widget.ffi.canvasModel.cursorEmbedded) { - displayMenu.add(RemoteMenuEntry.showRemoteCursor( - widget.id, - padding, - dismissCallback: _menuDismissCallback, - )); - } - - /// Show remote cursor scaling with image - if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) { - displayMenu.add(() { - final opt = 'zoom-cursor'; - final state = PeerBoolOption.find(widget.id, opt); - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Zoom cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: widget.id, value: opt); - state.value = - bind.sessionGetToggleOptionSync(id: widget.id, arg: opt); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ); - }()); - } - - /// Show quality monitor - displayMenu.add(MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translate('Show quality monitor'), - getter: () async { - return bind.sessionGetToggleOptionSync( - id: widget.id, arg: 'show-quality-monitor'); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'show-quality-monitor'); - widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - - final perms = widget.ffi.ffiModel.permissions; - final pi = widget.ffi.ffiModel.pi; - - if (perms['audio'] != false) { - displayMenu - .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); - } - - if (Platform.isWindows && - pi.platform == kPeerPlatformWindows && - perms['file'] != false) { - displayMenu.add(_createSwitchMenuEntry( - 'Allow file copy and paste', 'enable-file-transfer', padding, true)); - } - - if (perms['keyboard'] != false) { - if (perms['clipboard'] != false) { - displayMenu.add(RemoteMenuEntry.disableClipboard( - widget.id, - padding, - dismissCallback: _menuDismissCallback, - )); - } - displayMenu.add(_createSwitchMenuEntry( - 'Lock after session end', 'lock-after-session-end', padding, true)); - if (pi.features.privacyMode) { - displayMenu.add(MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Privacy mode'), - getter: () { - return PrivacyModeState.find(widget.id); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'privacy-mode'); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - } - return displayMenu; - } - - List> _getKeyboardMenu() { - final List> keyboardMenu = [ - MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () { - List list = []; - List modes = [ - KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'), - KeyboardModeMenu(key: 'map', menu: 'Map mode'), - KeyboardModeMenu(key: 'translate', menu: 'Translate mode'), - ]; - - for (KeyboardModeMenu mode in modes) { - if (bind.sessionIsKeyboardModeSupported( - id: widget.id, mode: mode.key)) { - if (mode.key == 'translate') { - if (Platform.isLinux || - widget.ffi.ffiModel.pi.platform == kPeerPlatformLinux) { - continue; - } - } - var text = translate(mode.menu); - if (mode.key == 'translate') { - text = '$text beta'; - } - list.add(MenuEntryRadioOption(text: text, value: mode.key)); - } - } - return list; - }, - curOptionGetter: () async { - return await bind.sessionGetKeyboardMode(id: widget.id) ?? 'legacy'; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetKeyboardMode(id: widget.id, value: newValue); - }, - ) - ]; - final localPlatform = - getLocalPlatformForKBLayoutType(widget.ffi.ffiModel.pi.platform); - if (localPlatform != '') { - keyboardMenu.add(MenuEntryDivider()); - keyboardMenu.add( - MenuEntryButton( - childBuilder: (TextStyle? style) => Container( - alignment: AlignmentDirectional.center, - height: _MenubarTheme.height, - child: Row( - children: [ - Obx(() => RichText( - text: TextSpan( - text: '${translate('Local keyboard type')}: ', - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan( - text: KBLayoutType.value, - style: TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - )), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: 0.8, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.settings), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - _menuDismissCallback(); - } - showKBLayoutTypeChooser( - localPlatform, widget.ffi.dialogManager); - }, - ), - ), - )) - ], - )), - proc: () {}, - padding: EdgeInsets.zero, - dismissOnClicked: false, - dismissCallback: _menuDismissCallback, - ), - ); - } - return keyboardMenu; - } - - MenuEntrySwitch _createSwitchMenuEntry( - String text, String option, EdgeInsets? padding, bool dismissOnClicked) { - return RemoteMenuEntry.createSwitchMenuEntry( - widget.id, text, option, padding, dismissOnClicked, - dismissCallback: _menuDismissCallback); - } -} - -void showSetOSPassword( - String id, bool login, OverlayDialogManager dialogManager) async { - final controller = TextEditingController(); - var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? ''; - var autoLogin = await bind.sessionGetOption(id: id, arg: 'auto-login') != ''; - controller.text = password; - dialogManager.show((setState, close) { - submit() { - var text = controller.text.trim(); - bind.sessionPeerOption(id: id, name: 'os-password', value: text); - bind.sessionPeerOption( - id: id, name: 'auto-login', value: autoLogin ? 'Y' : ''); - if (text != '' && login) { - bind.sessionInputOsPassword(id: id, value: text); - } - close(); - } - - return CustomAlertDialog( - title: Text(translate('OS Password')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - PasswordWidget(controller: controller), - CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('Auto Login'), - ), - value: autoLogin, - onChanged: (v) { - if (v == null) return; - setState(() => autoLogin = v); - }, - ), - ]), - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton('OK', onPressed: submit), - ], - onSubmit: submit, - onCancel: close, - ); - }); -} - -void showAuditDialog(String id, dialogManager) async { - final controller = TextEditingController(); - dialogManager.show((setState, close) { - submit() { - var text = controller.text.trim(); - if (text != '') { - bind.sessionSendNote(id: id, note: text); - } - close(); - } - - late final focusNode = FocusNode( - onKey: (FocusNode node, RawKeyEvent evt) { - if (evt.logicalKey.keyLabel == 'Enter') { - if (evt is RawKeyDownEvent) { - int pos = controller.selection.base.offset; - controller.text = - '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; - controller.selection = - TextSelection.fromPosition(TextPosition(offset: pos + 1)); - } - return KeyEventResult.handled; - } - if (evt.logicalKey.keyLabel == 'Esc') { - if (evt is RawKeyDownEvent) { - close(); - } - return KeyEventResult.handled; - } else { - return KeyEventResult.ignored; - } - }, - ); - - return CustomAlertDialog( - title: Text(translate('Note')), - content: SizedBox( - width: 250, - height: 120, - child: TextField( - autofocus: true, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.newline, - decoration: const InputDecoration.collapsed( - hintText: 'input note here', + return SubmenuButton( + child: Text(translate('Codec')), + menuChildren: [ + _RadioMenuButton( + child: Text(translate('Auto')), + value: 'auto', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, ), - // inputFormatters: [ - // LengthLimitingTextInputFormatter(16), - // // FilteringTextInputFormatter(RegExp(r'[a-zA-z][a-zA-z0-9\_]*'), allow: true) - // ], - maxLines: null, - maxLength: 256, - controller: controller, - focusNode: focusNode, - )), - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton('OK', onPressed: submit) - ], - onSubmit: submit, - onCancel: close, - ); - }); -} + _RadioMenuButton( + child: Text(translate('VP9')), + value: 'vp9', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('H264')), + value: 'h264', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('H265')), + value: 'h265', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList()); + }); + } -void showConfirmSwitchSidesDialog( - String id, OverlayDialogManager dialogManager) async { - dialogManager.show((setState, close) { - submit() async { - await bind.sessionSwitchSides(id: id); - closeConnection(id: id); + resolutions() { + final resolutions = widget.ffi.ffiModel.pi.resolutions; + final visible = widget.ffi.ffiModel.permissions["keyboard"] != false && + resolutions.length > 1; + if (!visible) return Offstage(); + final display = widget.ffi.ffiModel.display; + final groupValue = "${display.width}x${display.height}"; + onChanged(String? value) async { + if (value == null) return; + final list = value.split('x'); + if (list.length == 2) { + final w = int.tryParse(list[0]); + final h = int.tryParse(list[1]); + if (w != null && h != null) { + await bind.sessionChangeResolution( + id: widget.id, width: w, height: h); + } + } } - return CustomAlertDialog( - content: msgboxContent('info', 'Switch Sides', - 'Please confirm if you want to share your desktop?'), - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton('OK', onPressed: submit), + return SubmenuButton( + menuChildren: resolutions + .map((e) => _RadioMenuButton( + value: '${e.width}x${e.height}', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + child: Text('${e.width}x${e.height}'))) + .toList() + .map((e) => _buildPointerTrackWidget(e, widget.ffi)) + .toList(), + child: Text(translate("Resolution"))); + } + + showRemoteCursor() { + final visible = !widget.ffi.canvasModel.cursorEmbedded; + if (!visible) return Offstage(); + final state = ShowRemoteCursorState.find(widget.id); + final option = 'show-remote-cursor'; + return _CheckboxMenuButton( + value: state.value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: widget.id, value: option); + state.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + }, + ffi: widget.ffi, + child: Text(translate('Show remote cursor'))); + } + + zoomCursor() { + final visible = widget.state.viewStyle.value != kRemoteViewStyleOriginal; + if (!visible) return Offstage(); + final option = 'zoom-cursor'; + final peerState = PeerBoolOption.find(widget.id, option); + return _CheckboxMenuButton( + value: peerState.value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: widget.id, value: option); + peerState.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + }, + ffi: widget.ffi, + child: Text(translate('Zoom cursor'))); + } + + showQualityMonitor() { + final option = 'show-quality-monitor'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: widget.id, value: option); + widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); + }, + ffi: widget.ffi, + child: Text(translate('Show quality monitor'))); + } + + mute() { + final visible = perms['audio'] != false; + if (!visible) return Offstage(); + final option = 'disable-audio'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Mute'))); + } + + fileCopyAndPaste() { + final visible = Platform.isWindows && + pi.platform == kPeerPlatformWindows && + perms['file'] != false; + if (!visible) return Offstage(); + final option = 'enable-file-transfer'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Allow file copy and paste'))); + } + + disableClipboard() { + final visible = perms['keyboard'] != false && perms['clipboard'] != false; + if (!visible) return Offstage(); + final option = 'disable-clipboard'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Disable clipboard'))); + } + + lockAfterSessionEnd() { + final visible = perms['keyboard'] != false; + if (!visible) return Offstage(); + final option = 'lock-after-session-end'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Lock after session end'))); + } + + privacyMode() { + bool visible = perms['keyboard'] != false && pi.features.privacyMode; + if (!visible) return Offstage(); + final option = 'privacy-mode'; + final rxValue = PrivacyModeState.find(widget.id); + return _CheckboxMenuButton( + value: rxValue.value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Privacy mode'))); + } +} + +class _KeyboardMenu extends StatelessWidget { + final String id; + final FFI ffi; + _KeyboardMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + PeerInfo get pi => ffi.ffiModel.pi; + + @override + Widget build(BuildContext context) { + var ffiModel = Provider.of(context); + if (ffiModel.permissions['keyboard'] == false) return Offstage(); + // Do not support peer 1.1.9. + if (stateGlobal.grabKeyboard) { + bind.sessionSetKeyboardMode(id: id, value: 'map'); + return Offstage(); + } + return _IconSubmenuButton( + svg: "assets/keyboard.svg", + ffi: ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuChildren: [mode(), localKeyboardType()]); + } + + mode() { + return futureBuilder(future: () async { + return await bind.sessionGetKeyboardMode(id: id) ?? 'legacy'; + }(), hasData: (data) { + final groupValue = data as String; + List modes = [ + KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'), + KeyboardModeMenu(key: 'map', menu: 'Map mode'), + KeyboardModeMenu(key: 'translate', menu: 'Translate mode'), + ]; + List<_RadioMenuButton> list = []; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetKeyboardMode(id: id, value: value); + } + + for (KeyboardModeMenu mode in modes) { + if (bind.sessionIsKeyboardModeSupported(id: id, mode: mode.key)) { + if (mode.key == 'translate') { + if (Platform.isLinux || pi.platform == kPeerPlatformLinux) { + continue; + } + } + var text = translate(mode.menu); + if (mode.key == 'translate') { + text = '$text beta'; + } + list.add(_RadioMenuButton( + child: Text(text), + value: mode.key, + groupValue: groupValue, + onChanged: onChanged, + ffi: ffi, + )); + } + } + return Column(children: list); + }); + } + + localKeyboardType() { + final localPlatform = getLocalPlatformForKBLayoutType(pi.platform); + final visible = localPlatform != ''; + if (!visible) return Offstage(); + return Column( + children: [ + Divider(), + _MenuItemButton( + child: Text( + '${translate('Local keyboard type')}: ${KBLayoutType.value}'), + trailingIcon: const Icon(Icons.settings), + ffi: ffi, + onPressed: () => + showKBLayoutTypeChooser(localPlatform, ffi.dialogManager), + ) ], - onSubmit: submit, - onCancel: close, ); - }); + } +} + +class _ChatMenu extends StatefulWidget { + final String id; + final FFI ffi; + _ChatMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + @override + State<_ChatMenu> createState() => _ChatMenuState(); +} + +class _ChatMenuState extends State<_ChatMenu> { + // Using in StatelessWidget got `Looking up a deactivated widget's ancestor is unsafe`. + final chatButtonKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return _IconSubmenuButton( + key: chatButtonKey, + svg: 'assets/chat.svg', + ffi: widget.ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuChildren: [textChat(), voiceCall()]); + } + + textChat() { + return _MenuItemButton( + child: Text(translate('Text chat')), + ffi: widget.ffi, + onPressed: () { + RenderBox? renderBox = + chatButtonKey.currentContext?.findRenderObject() as RenderBox?; + + Offset? initPos; + if (renderBox != null) { + final pos = renderBox.localToGlobal(Offset.zero); + initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight); + } + + widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); + widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); + }); + } + + voiceCall() { + return _MenuItemButton( + child: Text(translate('Voice call')), + ffi: widget.ffi, + onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), + ); + } +} + +class _VoiceCallMenu extends StatelessWidget { + final String id; + final FFI ffi; + _VoiceCallMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx( + () { + final String tooltip; + final String icon; + switch (ffi.chatModel.voiceCallStatus.value) { + case VoiceCallStatus.waitingForResponse: + tooltip = "Waiting"; + icon = "assets/call_wait.svg"; + break; + case VoiceCallStatus.connected: + tooltip = "Disconnect"; + icon = "assets/call_end.svg"; + break; + default: + return Offstage(); + } + return _IconMenuButton( + assetName: icon, + tooltip: tooltip, + onPressed: () => bind.sessionCloseVoiceCall(id: id), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor); + }, + ); + } +} + +class _RecordMenu extends StatelessWidget { + const _RecordMenu({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + var ffi = Provider.of(context); + final visible = ffi.permissions['recording'] != false; + if (!visible) return Offstage(); + return Consumer( + builder: (context, value, child) => _IconMenuButton( + assetName: 'assets/rec.svg', + tooltip: + value.start ? 'Stop session recording' : 'Start session recording', + onPressed: () => value.toggle(), + color: value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor, + hoverColor: value.start + ? _MenubarTheme.hoverRedColor + : _MenubarTheme.hoverBlueColor, + ), + ); + } +} + +class _CloseMenu extends StatelessWidget { + final String id; + final FFI ffi; + const _CloseMenu({Key? key, required this.id, required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _IconMenuButton( + assetName: 'assets/close.svg', + tooltip: 'Close', + onPressed: () => clientClose(id, ffi.dialogManager), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor, + ); + } +} + +class _IconMenuButton extends StatefulWidget { + final String? assetName; + final Widget? icon; + final String tooltip; + final Color color; + final Color hoverColor; + final VoidCallback? onPressed; + final double? hMargin; + final double? vMargin; + const _IconMenuButton({ + Key? key, + this.assetName, + this.icon, + required this.tooltip, + required this.color, + required this.hoverColor, + required this.onPressed, + this.hMargin, + this.vMargin, + }) : super(key: key); + + @override + State<_IconMenuButton> createState() => _IconMenuButtonState(); +} + +class _IconMenuButtonState extends State<_IconMenuButton> { + bool hover = false; + + @override + Widget build(BuildContext context) { + assert(widget.assetName != null || widget.icon != null); + final icon = widget.icon ?? + SvgPicture.asset( + widget.assetName!, + color: Colors.white, + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + ); + return SizedBox( + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + child: MenuItemButton( + style: ButtonStyle( + padding: MaterialStatePropertyAll(EdgeInsets.zero), + overlayColor: MaterialStatePropertyAll(Colors.transparent)), + onHover: (value) => setState(() { + hover = value; + }), + onPressed: widget.onPressed, + child: Tooltip( + message: translate(widget.tooltip), + child: Material( + type: MaterialType.transparency, + child: Ink( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(_MenubarTheme.iconRadius), + color: hover ? widget.hoverColor : widget.color, + ), + child: icon))), + ), + ).marginSymmetric( + horizontal: widget.hMargin ?? _MenubarTheme.buttonHMargin, + vertical: widget.vMargin ?? _MenubarTheme.buttonVMargin); + } +} + +class _IconSubmenuButton extends StatefulWidget { + final String? svg; + final Widget? icon; + final Color color; + final Color hoverColor; + final List menuChildren; + final MenuStyle? menuStyle; + final FFI ffi; + + _IconSubmenuButton( + {Key? key, + this.svg, + this.icon, + required this.color, + required this.hoverColor, + required this.menuChildren, + required this.ffi, + this.menuStyle}) + : super(key: key); + + @override + State<_IconSubmenuButton> createState() => _IconSubmenuButtonState(); +} + +class _IconSubmenuButtonState extends State<_IconSubmenuButton> { + bool hover = false; + + @override + Widget build(BuildContext context) { + assert(widget.svg != null || widget.icon != null); + final icon = widget.icon ?? + SvgPicture.asset( + widget.svg!, + color: Colors.white, + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + ); + return SizedBox( + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + child: SubmenuButton( + menuStyle: widget.menuStyle, + style: ButtonStyle( + padding: MaterialStatePropertyAll(EdgeInsets.zero), + overlayColor: MaterialStatePropertyAll(Colors.transparent)), + onHover: (value) => setState(() { + hover = value; + }), + child: Material( + type: MaterialType.transparency, + child: Ink( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(_MenubarTheme.iconRadius), + color: hover ? widget.hoverColor : widget.color, + ), + child: icon)), + menuChildren: widget.menuChildren + .map((e) => _buildPointerTrackWidget(e, widget.ffi)) + .toList())) + .marginSymmetric( + horizontal: _MenubarTheme.buttonHMargin, + vertical: _MenubarTheme.buttonVMargin); + } +} + +class _MenuItemButton extends StatelessWidget { + final VoidCallback? onPressed; + final Widget? trailingIcon; + final Widget? child; + final FFI ffi; + _MenuItemButton( + {Key? key, + this.onPressed, + this.trailingIcon, + required this.child, + required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MenuItemButton( + key: key, + onPressed: onPressed != null + ? () { + _menuDismissCallback(ffi); + onPressed?.call(); + } + : null, + trailingIcon: trailingIcon, + child: child); + } +} + +class _CheckboxMenuButton extends StatelessWidget { + final bool? value; + final ValueChanged? onChanged; + final Widget? child; + final FFI ffi; + const _CheckboxMenuButton( + {Key? key, + required this.value, + required this.onChanged, + required this.child, + required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return CheckboxMenuButton( + key: key, + value: value, + child: child, + onChanged: onChanged != null + ? (bool? value) { + _menuDismissCallback(ffi); + onChanged?.call(value); + } + : null, + ); + } +} + +class _RadioMenuButton extends StatelessWidget { + final T value; + final T? groupValue; + final ValueChanged? onChanged; + final Widget? child; + final FFI ffi; + const _RadioMenuButton( + {Key? key, + required this.value, + required this.groupValue, + required this.onChanged, + required this.child, + required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return RadioMenuButton( + value: value, + groupValue: groupValue, + child: child, + onChanged: onChanged != null + ? (T? value) { + _menuDismissCallback(ffi); + onChanged?.call(value); + } + : null, + ); + } } class _DraggableShowHide extends StatefulWidget { @@ -1843,3 +2022,15 @@ class KeyboardModeMenu { KeyboardModeMenu({required this.key, required this.menu}); } + +_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos(); + +Widget _buildPointerTrackWidget(Widget child, FFI ffi) { + return Listener( + onPointerHover: (PointerHoverEvent e) => + ffi.inputModel.lastMousePos = e.position, + child: MouseRegion( + child: child, + ), + ); +} diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 45c552848..71aa39337 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 4824ac5e9..818e63203 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "停止语音通话"), ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在 ID 后面添加/r,或者在卡片选项里选择强制走中继连接。"), ("Reconnect", "重连"), + ("Codec", "编解码"), + ("Resolution", "分辨率"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e2761e45e..be0ffa7f4 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 2020a2b6f..150a57715 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 7cf563fc3..c9c25df2b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Sprachanruf beenden"), ("relay_hint_tip", "Wenn eine direkte Verbindung nicht möglich ist, können Sie versuchen, eine Verbindung über einen Relay-Server herzustellen. \nWenn Sie eine Relay-Verbindung beim ersten Versuch herstellen möchten, können Sie das Suffix \"/r\" an die ID anhängen oder die Option \"Immer über Relay-Server verbinden\" auf der Gegenstelle auswählen."), ("Reconnect", "Erneut verbinden"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index c22532440..bb2615efc 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 3ce2860f0..d7e43b6bf 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Detener llamada de voz"), ("relay_hint_tip", "Puede que no sea posible conectar directamente. Puedes tratar de conectar a través de relay. \nAdicionalmente, si quieres usar relay en el primer intento, puedes añadir el sufijo \"/r\" a la ID o seleccionar la opción \"Conectar siempre a través de relay\" en la tarjeta del par."), ("Reconnect", "Reconectar"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 70051f3e8..d8fcff436 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "توقف تماس صوتی"), ("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), ("Reconnect", "اتصال مجدد"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 1f6e9f55b..cb4d8d69f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index b7ebf4577..c18e6c07b 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 21ab28214..557e3faf0 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f48de17f6..1a34e6fea 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 4c63106da..7256b13d8 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Interrompi la chiamata vocale"), ("relay_hint_tip", "Se non è possibile connettersi direttamente, si può provare a farlo tramite relay.\nInoltre, se si desidera utilizzare il relay al primo tentativo, è possibile aggiungere il suffisso \"/r\" all'ID o selezionare l'opzione \"Collegati sempre tramite relay\" nella scheda peer."), ("Reconnect", "Riconnetti"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index b291a6e7a..d6354c1c9 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index d63e83187..dc57c8bf9 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index b8b9eb1df..6698b2c5f 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 1a806c803..545e1ec2e 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Stop spraakoproep"), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 2b29c7cb2..eea46accb 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e91cd3909..ee1561123 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index b0fe9175d..7b16bdf34 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index d0232ba37..315eadd2a 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index fe5d708ad..6d212490b 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Завершить голосовой вызов"), ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."), ("Reconnect", "Переподключить"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 458002f4c..462a78ab6 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 2abd1870f..0eb1949fe 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 6b739e8ab..2fc5dfe0d 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 90a435fd7..17882094c 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index a98ea6346..250cf3405 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 61c2b5d28..dcdcc1289 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 236ee5e8d..a1eb34c54 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f2a34e212..09c40a83f 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 84e74716f..ca1193eaa 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "停止語音聊天"), ("relay_hint_tip", "可能無法直連,可以嘗試中繼連接。 \n另外,如果想直接使用中繼連接,可以在ID後面添加/r,或者在卡片選項裡選擇強制走中繼連接。"), ("Reconnect", "重連"), + ("Codec", "編解碼"), + ("Resolution", "分辨率"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 0c4caf4db..b48385e6e 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 19e1184d9..61d7c0b8a 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } From f5edf44f0f9c28ea865647e9596b1b5bccbdae2b Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 23 Feb 2023 21:31:00 +0800 Subject: [PATCH 199/202] remote menubar theme Signed-off-by: 21pages --- .../lib/desktop/widgets/remote_menubar.dart | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 993d02683..b32520fa6 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -408,18 +408,32 @@ class _RemoteMenubarState extends State { ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, - child: MenuBar( - children: [ - SizedBox(width: _MenubarTheme.buttonHMargin), - ...menubarItems, - SizedBox(width: _MenubarTheme.buttonHMargin) - ], + child: Theme( + data: themeData(), + child: MenuBar( + children: [ + SizedBox(width: _MenubarTheme.buttonHMargin), + ...menubarItems, + SizedBox(width: _MenubarTheme.buttonHMargin) + ], + ), )), ), _buildDraggableShowHide(context), ], ); } + + ThemeData themeData() { + return Theme.of(context).copyWith( + menuButtonTheme: MenuButtonThemeData( + style: ButtonStyle( + minimumSize: MaterialStatePropertyAll(Size(64, 36)), + textStyle: MaterialStatePropertyAll( + TextStyle(fontWeight: FontWeight.normal)))), + dividerTheme: DividerThemeData(space: 4), + ); + } } class _PinMenu extends StatelessWidget { From 69f16ccd9f086cebfa2053f9df48c6d96b42c7d3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 23 Feb 2023 21:57:45 +0800 Subject: [PATCH 200/202] delay 3s to adjust window after changing resolution Signed-off-by: 21pages --- flutter/lib/desktop/widgets/remote_menubar.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index b32520fa6..4f9a227bd 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1351,6 +1351,14 @@ class _DisplayMenuState extends State<_DisplayMenu> { if (w != null && h != null) { await bind.sessionChangeResolution( id: widget.id, width: w, height: h); + Future.delayed(Duration(seconds: 3), () async { + final display = widget.ffi.ffiModel.display; + if (w == display.width && h == display.height) { + if (_isWindowCanBeAdjusted()) { + _doAdjustWindow(); + } + } + }); } } } From c3c4505132b3e7109361d02a1b6fa69a7377bd9b Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 24 Feb 2023 14:15:54 +0800 Subject: [PATCH 201/202] feat: make file manager draggable --- flutter/lib/consts.dart | 2 + .../lib/desktop/pages/file_manager_page.dart | 170 ++++++++++++------ .../lib/desktop/widgets/dragable_divider.dart | 53 ++++++ .../widgets/list_search_action_listener.dart | 1 + 4 files changed, 168 insertions(+), 58 deletions(-) create mode 100644 flutter/lib/desktop/widgets/dragable_divider.dart diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 2b73182fd..537784918 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -53,6 +53,8 @@ const int kDesktopMaxDisplayHeight = 1080; const double kDesktopFileTransferNameColWidth = 200; const double kDesktopFileTransferModifiedColWidth = 120; +const double kDesktopFileTransferMinimumWidth = 100; +const double kDesktopFileTransferMaximumWidth = 300; const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferHeaderHeight = 25.0; diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 0d55552af..68023f929 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; import 'package:percent_indicator/percent_indicator.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/gestures.dart'; @@ -78,6 +79,10 @@ class _FileManagerPageState extends State final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote"); final _listSearchBufferLocal = TimeoutStringBuffer(); final _listSearchBufferRemote = TimeoutStringBuffer(); + final _nameColWidthLocal = kDesktopFileTransferNameColWidth.obs; + final _modifiedColWidthLocal = kDesktopFileTransferModifiedColWidth.obs; + final _nameColWidthRemote = kDesktopFileTransferNameColWidth.obs; + final _modifiedColWidthRemote = kDesktopFileTransferModifiedColWidth.obs; /// [_lastClickTime], [_lastClickEntry] help to handle double click int _lastClickTime = @@ -297,11 +302,12 @@ class _FileManagerPageState extends State } var searchResult = entries .skip(skipCount) - .where((element) => element.name.startsWith(buffer)); + .where((element) => element.name.toLowerCase().startsWith(buffer)); if (searchResult.isEmpty) { // cannot find next, lets restart search from head + debugPrint("restart search from head"); searchResult = - entries.where((element) => element.name.startsWith(buffer)); + entries.where((element) => element.name.toLowerCase().startsWith(buffer)); } if (searchResult.isEmpty) { setState(() { @@ -316,7 +322,7 @@ class _FileManagerPageState extends State debugPrint("searching for $buffer"); final selectedEntries = getSelectedItems(isLocal); final searchResult = - entries.where((element) => element.name.startsWith(buffer)); + entries.where((element) => element.name.toLowerCase().startsWith(buffer)); selectedEntries.clear(); if (searchResult.isEmpty) { setState(() { @@ -362,37 +368,41 @@ class _FileManagerPageState extends State child: Row( children: [ GestureDetector( - child: Container( - width: kDesktopFileTransferNameColWidth, - child: Tooltip( - waitDuration: - Duration(milliseconds: 500), - message: entry.name, - child: Row(children: [ - entry.isDrive - ? Image( - image: iconHardDrive, - fit: BoxFit.scaleDown, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7)) - .paddingAll(4) - : SvgPicture.asset( - entry.isFile - ? "assets/file.svg" - : "assets/folder.svg", - color: Theme.of(context) - .tabBarTheme - .labelColor, - ), - Expanded( - child: Text( - entry.name.nonBreaking, - overflow: - TextOverflow.ellipsis)) - ]), - )), + child: Obx( + () => Container( + width: isLocal + ? _nameColWidthLocal.value + : _nameColWidthRemote.value, + child: Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: entry.name, + child: Row(children: [ + entry.isDrive + ? Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)) + .paddingAll(4) + : SvgPicture.asset( + entry.isFile + ? "assets/file.svg" + : "assets/folder.svg", + color: Theme.of(context) + .tabBarTheme + .labelColor, + ), + Expanded( + child: Text( + entry.name.nonBreaking, + overflow: + TextOverflow.ellipsis)) + ]), + )), + ), onTap: () { final items = getSelectedItems(isLocal); // handle double click @@ -406,24 +416,35 @@ class _FileManagerPageState extends State items, filteredEntries, entry, isLocal); }, ), + SizedBox( + width: 2.0, + ), GestureDetector( - child: SizedBox( - width: kDesktopFileTransferModifiedColWidth, - child: Tooltip( - waitDuration: - Duration(milliseconds: 500), - message: lastModifiedStr, - child: Text( - lastModifiedStr, - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - )), + child: Obx( + () => SizedBox( + width: isLocal + ? _modifiedColWidthLocal.value + : _modifiedColWidthRemote.value, + child: Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + )), + ), ), ), + // Divider from header. SizedBox( - width: 100, + width: 2.0, + ), + Expanded( + // width: 100, child: GestureDetector( child: Tooltip( waitDuration: Duration(milliseconds: 500), @@ -1362,6 +1383,7 @@ class _FileManagerPageState extends State Text( name, style: headerTextStyle, + overflow: TextOverflow.ellipsis, ).marginSymmetric(horizontal: 4), ascending.value != null ? Icon( @@ -1383,16 +1405,48 @@ class _FileManagerPageState extends State } Widget _buildFileBrowserHeader(BuildContext context, bool isLocal) { - return Row( - children: [ - headerItemFunc(kDesktopFileTransferNameColWidth, SortBy.name, - translate("Name"), isLocal), - headerItemFunc(kDesktopFileTransferModifiedColWidth, SortBy.modified, - translate("Modified"), isLocal), - Expanded( - child: - headerItemFunc(null, SortBy.size, translate("Size"), isLocal)) - ], + final nameColWidth = isLocal ? _nameColWidthLocal : _nameColWidthRemote; + final modifiedColWidth = + isLocal ? _modifiedColWidthLocal : _modifiedColWidthRemote; + final padding = EdgeInsets.all(1.0); + return SizedBox( + height: kDesktopFileTransferHeaderHeight, + child: Row( + children: [ + Obx( + () => headerItemFunc( + nameColWidth.value, SortBy.name, translate("Name"), isLocal), + ), + DraggableDivider( + axis: Axis.vertical, + onPointerMove: (dx) { + nameColWidth.value += dx; + nameColWidth.value = min( + kDesktopFileTransferMaximumWidth, + max(kDesktopFileTransferMinimumWidth, + nameColWidth.value)); + }, + padding: padding, + ), + Obx( + () => headerItemFunc(modifiedColWidth.value, SortBy.modified, + translate("Modified"), isLocal), + ), + DraggableDivider( + axis: Axis.vertical, + onPointerMove: (dx) { + modifiedColWidth.value += dx; + modifiedColWidth.value = min( + kDesktopFileTransferMaximumWidth, + max(kDesktopFileTransferMinimumWidth, + modifiedColWidth.value)); + }, + padding: padding), + Expanded( + child: + headerItemFunc(null, SortBy.size, translate("Size"), isLocal)) + ], + ), ); } } diff --git a/flutter/lib/desktop/widgets/dragable_divider.dart b/flutter/lib/desktop/widgets/dragable_divider.dart new file mode 100644 index 000000000..3821b7e0d --- /dev/null +++ b/flutter/lib/desktop/widgets/dragable_divider.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; + +class DraggableDivider extends StatefulWidget { + final Axis axis; + final double thickness; + final Color color; + final Function(double)? onPointerMove; + final VoidCallback? onHover; + final EdgeInsets padding; + const DraggableDivider({ + super.key, + this.axis = Axis.horizontal, + this.thickness = 1.0, + this.color = const Color.fromARGB(200, 177, 175, 175), + this.onPointerMove, + this.padding = const EdgeInsets.symmetric(horizontal: 1.0), + this.onHover, + }); + + @override + State createState() => _DraggableDividerState(); +} + +class _DraggableDividerState extends State { + @override + Widget build(BuildContext context) { + return Listener( + onPointerMove: (event) { + final dl = + widget.axis == Axis.horizontal ? event.localDelta.dy : event.localDelta.dx; + widget.onPointerMove?.call(dl); + }, + onPointerHover: (event) => widget.onHover?.call(), + child: MouseRegion( + cursor: SystemMouseCursors.resizeLeftRight, + child: Padding( + padding: widget.padding, + child: Container( + decoration: BoxDecoration(color: widget.color), + width: widget.axis == Axis.horizontal + ? double.infinity + : widget.thickness, + height: widget.axis == Axis.horizontal + ? widget.thickness + : double.infinity, + ), + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/widgets/list_search_action_listener.dart b/flutter/lib/desktop/widgets/list_search_action_listener.dart index 9598c3400..36128bf26 100644 --- a/flutter/lib/desktop/widgets/list_search_action_listener.dart +++ b/flutter/lib/desktop/widgets/list_search_action_listener.dart @@ -55,6 +55,7 @@ class TimeoutStringBuffer { } ListSearchAction input(String ch) { + ch = ch.toLowerCase(); final curr = DateTime.now(); try { if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) { From b10c0ffe54c30649a7e3394a7ccf1f03295cd59d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 24 Feb 2023 15:56:37 +0800 Subject: [PATCH 202/202] opt: fs explorer resizable & search next for loop --- .../lib/desktop/pages/file_manager_page.dart | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 68023f929..569e1cb9a 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -316,7 +316,7 @@ class _FileManagerPageState extends State return; } _jumpToEntry(isLocal, searchResult.first, scrollController, - kDesktopFileTransferRowHeight, buffer); + kDesktopFileTransferRowHeight); }, onSearch: (buffer) { debugPrint("searching for $buffer"); @@ -331,7 +331,7 @@ class _FileManagerPageState extends State return; } _jumpToEntry(isLocal, searchResult.first, scrollController, - kDesktopFileTransferRowHeight, buffer); + kDesktopFileTransferRowHeight); }, child: ObxValue( (searchText) { @@ -471,7 +471,11 @@ class _FileManagerPageState extends State return Column( children: [ // Header - _buildFileBrowserHeader(context, isLocal), + Row( + children: [ + Expanded(child: _buildFileBrowserHeader(context, isLocal)), + ], + ), // Body Expanded( child: ListView.builder( @@ -493,7 +497,7 @@ class _FileManagerPageState extends State } void _jumpToEntry(bool isLocal, Entry entry, - ScrollController scrollController, double rowHeight, String buffer) { + ScrollController scrollController, double rowHeight) { final entries = model.getCurrentDir(isLocal).entries; final index = entries.indexOf(entry); if (index == -1) { @@ -501,7 +505,7 @@ class _FileManagerPageState extends State } final selectedEntries = getSelectedItems(isLocal); final searchResult = - entries.where((element) => element.name.startsWith(buffer)); + entries.where((element) => element == entry); selectedEntries.clear(); if (searchResult.isEmpty) { return; @@ -1380,18 +1384,23 @@ class _FileManagerPageState extends State height: kDesktopFileTransferHeaderHeight, child: Row( children: [ - Text( - name, - style: headerTextStyle, - overflow: TextOverflow.ellipsis, - ).marginSymmetric(horizontal: 4), - ascending.value != null + Flexible( + flex: 2, + child: Text( + name, + style: headerTextStyle, + overflow: TextOverflow.ellipsis, + ).marginSymmetric(horizontal: 4), + ), + Flexible( + flex: 1, + child: ascending.value != null ? Icon( ascending.value! ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded, ) - : const Offstage() + : const Offstage()) ], ), ),